From cea821f9ca34c270a5ccc047342c2c21ae79a6c0 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 28 Aug 2020 17:42:12 +0200 Subject: A prototype to fix #364 --- src/python/gudhi/simplex_tree.pxd | 3 +++ src/python/gudhi/simplex_tree.pyx | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index 75e94e0b..44533d7f 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -66,6 +66,9 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": vector[Simplex_tree_simplex_handle].const_iterator get_filtration_iterator_end() nogil Simplex_tree_skeleton_iterator get_skeleton_iterator_begin(int dimension) nogil Simplex_tree_skeleton_iterator get_skeleton_iterator_end(int dimension) nogil + # + ctypedef bool (*blocker_func)(vector[int], void *user_data) + void expansion_with_blockers_callback(int dimension, blocker_func user_func, void *user_data) cdef extern from "Persistent_cohomology_interface.h" namespace "Gudhi": cdef cppclass Simplex_tree_persistence_interface "Gudhi::Persistent_cohomology_interface>": diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index dfb1d985..e297cfbd 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -17,6 +17,9 @@ __author__ = "Vincent Rouvreau" __copyright__ = "Copyright (C) 2016 Inria" __license__ = "MIT" +cdef bool callback(vector[int] simplex, void *blocker_func): + return (blocker_func)(simplex) + # SimplexTree python interface cdef class SimplexTree: """The simplex tree is an efficient and flexible data structure for @@ -409,6 +412,24 @@ cdef class SimplexTree: persistence_result = self.pcohptr.get_persistence() return self.get_ptr().compute_extended_persistence_subdiagrams(persistence_result, min_persistence) + def expansion_with_blocker(self, max_dim, blocker_func): + """Expands the Simplex_tree containing only its one skeleton + until dimension max_dim. + + The expanded simplicial complex until dimension :math:`d` + attached to a graph :math:`G` is the maximal simplicial complex of + dimension at most :math:`d` admitting the graph :math:`G` as + :math:`1`-skeleton. + The filtration value assigned to a simplex is the maximal filtration + value of one of its edges. + + The Simplex_tree must contain no simplex of dimension bigger than + 1 when calling the method. + + :param max_dim: The maximal dimension. + :type max_dim: int + """ + self.get_ptr().expansion_with_blockers_callback(max_dim, callback, blocker_func) def persistence(self, homology_coeff_field=11, min_persistence=0, persistence_dim_max = False): """This function computes and returns the persistence of the simplicial complex. -- cgit v1.2.3 From c2eb0484191f89fcbe40bc4ab04943eb808f12a9 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 1 Sep 2020 14:51:25 +0200 Subject: expansion with blocker and how to modify filtration value --- src/python/gudhi/simplex_tree.pxd | 2 +- src/python/gudhi/simplex_tree.pyx | 23 +++++++++--------- src/python/test/test_simplex_tree.py | 46 +++++++++++++++++++++--------------- 3 files changed, 40 insertions(+), 31 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index 44533d7f..80c6ffca 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -66,7 +66,7 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": vector[Simplex_tree_simplex_handle].const_iterator get_filtration_iterator_end() nogil Simplex_tree_skeleton_iterator get_skeleton_iterator_begin(int dimension) nogil Simplex_tree_skeleton_iterator get_skeleton_iterator_end(int dimension) nogil - # + # Expansion with blockers ctypedef bool (*blocker_func)(vector[int], void *user_data) void expansion_with_blockers_callback(int dimension, blocker_func user_func, void *user_data) diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index e297cfbd..debe92c0 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -413,21 +413,22 @@ cdef class SimplexTree: return self.get_ptr().compute_extended_persistence_subdiagrams(persistence_result, min_persistence) def expansion_with_blocker(self, max_dim, blocker_func): - """Expands the Simplex_tree containing only its one skeleton - until dimension max_dim. + """Expands the Simplex_tree containing only a graph. Simplices corresponding to cliques in the graph are added + incrementally, faces before cofaces, unless the simplex has dimension larger than `max_dim` or `blocker_func` + returns `True` for this simplex. - The expanded simplicial complex until dimension :math:`d` - attached to a graph :math:`G` is the maximal simplicial complex of - dimension at most :math:`d` admitting the graph :math:`G` as - :math:`1`-skeleton. - The filtration value assigned to a simplex is the maximal filtration - value of one of its edges. + The function identifies a candidate simplex whose faces are all already in the complex, inserts it with a + filtration value corresponding to the maximum of the filtration values of the faces, then calls `blocker_func` + with this new simplex (represented as a list of int). If `blocker_func` returns `True`, the simplex is removed, + otherwise it is kept. The algorithm then proceeds with the next candidate. - The Simplex_tree must contain no simplex of dimension bigger than - 1 when calling the method. + Note that you cannot update the filtration value of the simplex during the evaluation of `blocker_func`, as it + would segfault. - :param max_dim: The maximal dimension. + :param max_dim: Expansion maximal dimension value. :type max_dim: int + :param blocker_func: Blocker oracle. + :type blocker_func: Its concept is `Boolean blocker_func(list of int)` """ self.get_ptr().expansion_with_blockers_callback(max_dim, callback, blocker_func) diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index 7aad8259..33b0ac99 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -359,16 +359,6 @@ def test_collapse_edges(): for simplex in st.get_skeleton(0): assert simplex[1] == 1. -def blocker(simplex): - try: - # Block all simplices that countains vertex 6 - simplex.index(6) - print(simplex, ' is blocked') - return True - except ValueError: - print(simplex, ' is accepted') - return False - def test_expansion_with_blocker(): st=SimplexTree() st.insert([0,1],0) @@ -384,21 +374,39 @@ def test_expansion_with_blocker(): st.insert([5,6],10) st.insert([6],10) + # One cannot modify filtration inside blocker - segfault - Let's record accepted simplices + accepted_simp = [] + def blocker(simplex): + try: + # Block all simplices that countains vertex 6 + simplex.index(6) + print(simplex, ' is blocked') + return True + except ValueError: + print(simplex, ' is accepted') + accepted_simp.append(simplex) + return False + st.expansion_with_blocker(2, blocker) + for simplex in accepted_simp: + st.assign_filtration(simplex, st.filtration(simplex) + 1.) assert st.num_simplices() == 22 assert st.dimension() == 2 assert st.find([4,5,6]) == False - assert st.filtration([0,1,2]) == 3. - assert st.filtration([0,1,3]) == 4. - assert st.filtration([0,2,3]) == 5. - assert st.filtration([1,2,3]) == 5. + assert st.filtration([0,1,2]) == 4. + assert st.filtration([0,1,3]) == 5. + assert st.filtration([0,2,3]) == 6. + assert st.filtration([1,2,3]) == 6. + accepted_simp = [] st.expansion_with_blocker(3, blocker) + for simplex in accepted_simp: + st.assign_filtration(simplex, st.filtration(simplex) + 1.) assert st.num_simplices() == 23 assert st.dimension() == 3 assert st.find([4,5,6]) == False - assert st.filtration([0,1,2]) == 3. - assert st.filtration([0,1,3]) == 4. - assert st.filtration([0,2,3]) == 5. - assert st.filtration([1,2,3]) == 5. - assert st.filtration([0,1,2,3]) == 5. + assert st.filtration([0,1,2]) == 4. + assert st.filtration([0,1,3]) == 5. + assert st.filtration([0,2,3]) == 6. + assert st.filtration([1,2,3]) == 6. + assert st.filtration([0,1,2,3]) == 6. -- cgit v1.2.3 From f97865b2f5a0457d98bfd75eea3abc23e249943a Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 20 Dec 2020 14:48:33 +0100 Subject: More flexible Betti curve computations. Introduce a new BettiCurve2 class that can compute Betti curves on any grid (not just np.linspace ones), and can compute the grid needed to capture all values of the Betti curves. Based on feedback from PR #427. --- src/python/gudhi/representations/vector_methods.py | 152 ++++++++++++++++++++- 1 file changed, 151 insertions(+), 1 deletion(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index cdcb1fde..fda0a22d 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -1,14 +1,16 @@ # 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): Mathieu Carrière, Martin Royer +# Author(s): Mathieu Carrière, Martin Royer, Gard Spreemann # # Copyright (C) 2018-2020 Inria # # Modification(s): # - 2020/06 Martin: ATOL integration +# - 2020/12 Gard: A more flexible Betti curve class capable of computing exact curves. import numpy as np from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.exceptions import NotFittedError from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler from sklearn.neighbors import DistanceMetric from sklearn.metrics import pairwise @@ -350,6 +352,154 @@ class BettiCurve(BaseEstimator, TransformerMixin): """ return self.fit_transform([diag])[0,:] + +class BettiCurve2(BaseEstimator, TransformerMixin): + """ + A more flexible replacement for the BettiCurve class. + + Examples + -------- + If pd is a persistence diagram and xs is a grid such that xs[0] >= pd.min(), then the result of + >>> bc = BettiCurve2(xs) + >>> result = bc(pd) + and + >>> from scipy.interpolate import interp1d + >>> bc = BettiCurve2(None) + >>> bettis = bc.fit_transform([pd]) + >>> interp = interp1d(bc.grid_, bettis[0, :], kind="previous", fill_value="extrapolate") + >>> result = np.array(interp(xs), dtype=int) + are the same. + """ + + def __init__(self, grid = None): + """ + Constructor for the BettiCurve class. + + Parameters + ---------- + grid: 1d array or None, default=None + Filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinites are OK. If None (default), a grid will be computed that captures all the filtration value changes. + + Attributes + ---------- + grid_: 1d array + Contains the compute grid after fit or fit_transform. + """ + + self.grid_ = np.array(grid) + + + def fit(self, X, y = None): + """ + Compute a filtration grid that captures all changes in Betti numbers for all the given persistence diagrams. + + Parameters + ---------- + X: list of 2d arrays + Persistence diagrams. + + y: None. + Ignored. + """ + + events = np.unique(np.concatenate([pd.flatten() for pd in X], axis=0)) + + if len(events) == 0: + self.grid_ = np.array([-np.inf]) + else: + self.grid_ = np.array(events) + + return self + + + def fit_transform(self, X): + """ + Find a sampling grid that captures all changes in Betti numbers, and compute those Betti numbers. The result is the same as fit(X) followed by transform(X), but potentially faster. + """ + + N = len(X) + + events = np.concatenate([pd.flatten(order="F") for pd in X], axis=0) + sorting = np.argsort(events) + offsets = np.zeros(1 + N, dtype=int) + for i in range(0, N): + offsets[i+1] = offsets[i] + 2*X[i].shape[0] + starts = offsets[0:N] + ends = offsets[1:N + 1] - 1 + + bettis = [[0] for i in range(0, N)] + if len(sorting) == 0: + xs = [-np.inf] + else: + xs = [events[sorting[0]]] + + for i in sorting: + j = np.searchsorted(ends, i) + delta = 1 if i - starts[j] < len(X[j]) else -1 + if events[i] == xs[-1]: + bettis[j][-1] += delta + else: + xs.append(events[i]) + for k in range(0, j): + bettis[k].append(bettis[k][-1]) + bettis[j].append(bettis[j][-1] + delta) + for k in range(j+1, N): + bettis[k].append(bettis[k][-1]) + + self.grid_ = np.array(xs) + return np.array(bettis, dtype=int) + + + def transform(self, X): + """ + Compute Betti curves. + + Parameters + ---------- + X: list of 2d arrays + Persistence diagrams. + + Returns + ------- + (len(X))x(len(self.grid_)) array of ints + Betti numbers of the given persistence diagrams at the grid points given in self.grid_. + """ + + if self.grid_ is None: + raise NotFittedError("Not fitted. You need to call fit or construct with a chosen sampling grid.") + + N = len(X) + + events = np.concatenate([pd.flatten(order="F") for pd in X], axis=0) + sorting = np.argsort(events) + offsets = np.zeros(1 + N, dtype=int) + for i in range(0, N): + offsets[i+1] = offsets[i] + 2*X[i].shape[0] + starts = offsets[0:N] + ends = offsets[1:N + 1] - 1 + + bettis = [[0] for i in range(0, N)] + + i = 0 + for x in self.grid_: + while i < len(sorting) and events[sorting[i]] <= x: + j = np.searchsorted(ends, sorting[i]) + delta = 1 if sorting[i] - starts[j] < len(X[j]) else -1 + bettis[j][-1] += delta + i += 1 + for k in range(0, N): + bettis[k].append(bettis[k][-1]) + + return np.array(bettis, dtype=int)[:, 0:-1] + + + def __call__(self, diag): + """ + Shorthand for transform on a single persistence diagram. + """ + return self.transform([diag])[0, :] + + class Entropy(BaseEstimator, TransformerMixin): """ This is a class for computing persistence entropy. Persistence entropy is a statistic for persistence diagrams inspired from Shannon entropy. This statistic can also be used to compute a feature vector, called the entropy summary function. See https://arxiv.org/pdf/1803.08304.pdf for more details. Note that a previous implementation was contributed by Manuel Soriano-Trigueros. -- cgit v1.2.3 From ccb63b32bc65c0a6030dfab0b70ece62d9eff988 Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 28 Feb 2021 16:14:54 +0100 Subject: Move documentation string to class --- src/python/gudhi/representations/vector_methods.py | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index fda0a22d..13630360 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -357,6 +357,16 @@ class BettiCurve2(BaseEstimator, TransformerMixin): """ A more flexible replacement for the BettiCurve class. + Parameters + ---------- + grid: 1d array or None, default=None + Filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinites are OK. If None (default), a grid will be computed that captures all the filtration value changes. + + Attributes + ---------- + grid_: 1d array + Contains the compute grid after fit or fit_transform. + Examples -------- If pd is a persistence diagram and xs is a grid such that xs[0] >= pd.min(), then the result of @@ -372,20 +382,6 @@ class BettiCurve2(BaseEstimator, TransformerMixin): """ def __init__(self, grid = None): - """ - Constructor for the BettiCurve class. - - Parameters - ---------- - grid: 1d array or None, default=None - Filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinites are OK. If None (default), a grid will be computed that captures all the filtration value changes. - - Attributes - ---------- - grid_: 1d array - Contains the compute grid after fit or fit_transform. - """ - self.grid_ = np.array(grid) -- cgit v1.2.3 From fddeb5724fe2e7f1f37476c5e3cfade992a4edec Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 28 Feb 2021 18:40:46 +0100 Subject: Behave in line with scikit-learn guidelines According to [1], we should in particular not do any validation in the constructor, and fit/fit_transform should always update underscored attributes (self.grid_ in this case). We still want to allow for a user-defined, data-independent grid, so we make this a separate parameter predefined_grid. [1] https://scikit-learn.org/stable/developers/develop.html --- src/python/gudhi/representations/vector_methods.py | 86 ++++++++++++---------- 1 file changed, 46 insertions(+), 40 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 13630360..62a467c0 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -359,13 +359,13 @@ class BettiCurve2(BaseEstimator, TransformerMixin): Parameters ---------- - grid: 1d array or None, default=None - Filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinites are OK. If None (default), a grid will be computed that captures all the filtration value changes. + predefined_grid: 1d array or None, default=None + Predefined filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinities are OK. If None (default), a grid will be computed that captures all changes in Betti numbers in the provided data. Attributes ---------- grid_: 1d array - Contains the compute grid after fit or fit_transform. + The grid on which the Betti numbers are computed. If predefined_grid was specified, grid_ will always be that grid, independently of data. If not, the grid is fitted to capture all filtration values at which the Betti numbers change. Examples -------- @@ -381,13 +381,17 @@ class BettiCurve2(BaseEstimator, TransformerMixin): are the same. """ - def __init__(self, grid = None): - self.grid_ = np.array(grid) + def __init__(self, predefined_grid = None): + self.predefined_grid = predefined_grid + + + def is_fitted(self): + return hasattr(self, "grid_") def fit(self, X, y = None): """ - Compute a filtration grid that captures all changes in Betti numbers for all the given persistence diagrams. + Compute a filtration grid that captures all changes in Betti numbers for all the given persistence diagrams, unless a predefined grid was provided. Parameters ---------- @@ -398,12 +402,11 @@ class BettiCurve2(BaseEstimator, TransformerMixin): Ignored. """ - events = np.unique(np.concatenate([pd.flatten() for pd in X], axis=0)) - - if len(events) == 0: - self.grid_ = np.array([-np.inf]) - else: + if self.predefined_grid is None: + events = np.unique(np.concatenate([pd.flatten() for pd in X] + [[-np.inf]], axis=0)) self.grid_ = np.array(events) + else: + self.grid_ = np.array(self.predefined_grid) return self @@ -413,37 +416,39 @@ class BettiCurve2(BaseEstimator, TransformerMixin): Find a sampling grid that captures all changes in Betti numbers, and compute those Betti numbers. The result is the same as fit(X) followed by transform(X), but potentially faster. """ - N = len(X) + if self.predefined_grid is None: + N = len(X) - events = np.concatenate([pd.flatten(order="F") for pd in X], axis=0) - sorting = np.argsort(events) - offsets = np.zeros(1 + N, dtype=int) - for i in range(0, N): - offsets[i+1] = offsets[i] + 2*X[i].shape[0] - starts = offsets[0:N] - ends = offsets[1:N + 1] - 1 + events = np.concatenate([pd.flatten(order="F") for pd in X], axis=0) + sorting = np.argsort(events) + offsets = np.zeros(1 + N, dtype=int) + for i in range(0, N): + offsets[i+1] = offsets[i] + 2*X[i].shape[0] + starts = offsets[0:N] + ends = offsets[1:N + 1] - 1 - bettis = [[0] for i in range(0, N)] - if len(sorting) == 0: xs = [-np.inf] - else: - xs = [events[sorting[0]]] + bettis = [[0] for i in range(0, N)] + + for i in sorting: + j = np.searchsorted(ends, i) + delta = 1 if i - starts[j] < len(X[j]) else -1 + if events[i] == xs[-1]: + bettis[j][-1] += delta + else: + xs.append(events[i]) + for k in range(0, j): + bettis[k].append(bettis[k][-1]) + bettis[j].append(bettis[j][-1] + delta) + for k in range(j+1, N): + bettis[k].append(bettis[k][-1]) + + self.grid_ = np.array(xs) + return np.array(bettis, dtype=int) - for i in sorting: - j = np.searchsorted(ends, i) - delta = 1 if i - starts[j] < len(X[j]) else -1 - if events[i] == xs[-1]: - bettis[j][-1] += delta - else: - xs.append(events[i]) - for k in range(0, j): - bettis[k].append(bettis[k][-1]) - bettis[j].append(bettis[j][-1] + delta) - for k in range(j+1, N): - bettis[k].append(bettis[k][-1]) - - self.grid_ = np.array(xs) - return np.array(bettis, dtype=int) + else: + self.grid_ = self.predefined_grid + return self.transform(X) def transform(self, X): @@ -461,8 +466,8 @@ class BettiCurve2(BaseEstimator, TransformerMixin): Betti numbers of the given persistence diagrams at the grid points given in self.grid_. """ - if self.grid_ is None: - raise NotFittedError("Not fitted. You need to call fit or construct with a chosen sampling grid.") + if not self.is_fitted(): + raise NotFittedError("Not fitted.") N = len(X) @@ -496,6 +501,7 @@ class BettiCurve2(BaseEstimator, TransformerMixin): return self.transform([diag])[0, :] + class Entropy(BaseEstimator, TransformerMixin): """ This is a class for computing persistence entropy. Persistence entropy is a statistic for persistence diagrams inspired from Shannon entropy. This statistic can also be used to compute a feature vector, called the entropy summary function. See https://arxiv.org/pdf/1803.08304.pdf for more details. Note that a previous implementation was contributed by Manuel Soriano-Trigueros. -- cgit v1.2.3 From 482c36c28b1feaf65a2f26b0ee9ad2f4ddfae86c Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 28 Feb 2021 22:56:19 +0100 Subject: More precise interpolation invariant documentation text --- src/python/gudhi/representations/vector_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 62a467c0..a82c0d3c 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -369,7 +369,7 @@ class BettiCurve2(BaseEstimator, TransformerMixin): Examples -------- - If pd is a persistence diagram and xs is a grid such that xs[0] >= pd.min(), then the result of + If pd is a persistence diagram and xs is a nonempty grid of finite values such that xs[0] >= pd.min(), then the result of >>> bc = BettiCurve2(xs) >>> result = bc(pd) and -- cgit v1.2.3 From 79f002efaa1584e89f85928e464dd73ea64593b6 Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 28 Feb 2021 23:08:17 +0100 Subject: Elaborate doc string --- src/python/gudhi/representations/vector_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index a82c0d3c..5133a64c 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -355,7 +355,7 @@ class BettiCurve(BaseEstimator, TransformerMixin): class BettiCurve2(BaseEstimator, TransformerMixin): """ - A more flexible replacement for the BettiCurve class. + A more flexible replacement for the BettiCurve class. There are two modes of operation: with a predefined grid, and without. With a predefined grid, the class computes the Betti numbers at those grid points. Without a predefined grid, it can be fit to a list of persistence diagrams and produce a grid that consists of (at least) the filtration values at which at least one of those persistence diagrams chance Betti numbers, and then compute the Betti numbers at those grid points. In the latter mode, the exact Betti curve is computed for the entire real line. Parameters ---------- -- cgit v1.2.3 From a71694354af45e8edc2e2d2b4e14795bf9b5e5f1 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 12 Mar 2021 15:46:36 +0100 Subject: review constructor and test with one point and without points --- src/python/gudhi/alpha_complex.pyx | 8 +++----- src/python/test/test_alpha_complex.py | 5 +++++ 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index ea128743..f5f0ca5b 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -71,20 +71,18 @@ cdef class AlphaComplex: """ # The real cython constructor - def __cinit__(self, points = None, off_file = '', precision = 'safe'): + def __cinit__(self, points = [], off_file = '', precision = 'safe'): assert precision in ['fast', 'safe', 'exact'], "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' - cdef vector[vector[double]] pts if off_file: if os.path.isfile(off_file): points = read_points_from_off_file(off_file = off_file) else: print("file " + off_file + " not found.") - if points is None: - # Empty Alpha construction - points=[] + # need to copy the points to use them without the gil + cdef vector[vector[double]] pts pts = points with nogil: self.this_ptr = new Alpha_complex_interface(pts, fast, exact) diff --git a/src/python/test/test_alpha_complex.py b/src/python/test/test_alpha_complex.py index 814f8289..8f1424ec 100755 --- a/src/python/test/test_alpha_complex.py +++ b/src/python/test/test_alpha_complex.py @@ -25,12 +25,17 @@ __license__ = "MIT" def _empty_alpha(precision): + alpha_complex = gd.AlphaComplex(precision = precision) + assert alpha_complex.__is_defined() == True + +def _one_2d_point_alpha(precision): alpha_complex = gd.AlphaComplex(points=[[0, 0]], precision = precision) assert alpha_complex.__is_defined() == True def test_empty_alpha(): for precision in ['fast', 'safe', 'exact']: _empty_alpha(precision) + _one_2d_point_alpha(precision) def _infinite_alpha(precision): point_list = [[0, 0], [1, 0], [0, 1], [1, 1]] -- cgit v1.2.3 From a1674773ce5ed9060e5ecf173e5b75cd22f85b1a Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 12 Mar 2021 16:15:48 +0100 Subject: Reword alpha complex constructor documentation --- src/python/gudhi/alpha_complex.pyx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index f5f0ca5b..0fea3f37 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -61,9 +61,8 @@ cdef class AlphaComplex: :param points: A list of points in d-Dimension. :type points: list of list of double - Or - - :param off_file: An OFF file style name. + :param off_file: An `OFF file style `_ name. `points` are + read and overwritten by the points in the `off_file`. :type off_file: string :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is 'safe'. -- cgit v1.2.3 From d2ae3d4e9f17649813f64bbc3b00d540b23f21dd Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Mon, 15 Mar 2021 19:19:58 +0100 Subject: Add read_weights in reader_utils. Add exceptions for file not found and inconsistency in alpha ctor. Add UT accordingly --- src/python/gudhi/alpha_complex.pyx | 41 ++++++++++++++++++++++---- src/python/gudhi/reader_utils.pyx | 16 ++++++++++ src/python/test/test_alpha_complex.py | 55 +++++++++++++++++++++++++++++++++++ src/python/test/test_reader_utils.py | 49 +++++++++++++++++++++---------- 4 files changed, 139 insertions(+), 22 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index 0fea3f37..b7c20f74 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -16,11 +16,12 @@ from libcpp.utility cimport pair from libcpp.string cimport string from libcpp cimport bool from libc.stdint cimport intptr_t +import errno import os from gudhi.simplex_tree cimport * from gudhi.simplex_tree import SimplexTree -from gudhi import read_points_from_off_file +from gudhi import read_points_from_off_file, read_weights __author__ = "Vincent Rouvreau" __copyright__ = "Copyright (C) 2016 Inria" @@ -55,7 +56,7 @@ cdef class AlphaComplex: cdef Alpha_complex_interface * this_ptr # Fake constructor that does nothing but documenting the constructor - def __init__(self, points=None, off_file='', precision='safe'): + def __init__(self, points=[], off_file='', weights=[], weight_file='', precision='safe'): """AlphaComplex constructor. :param points: A list of points in d-Dimension. @@ -65,12 +66,24 @@ cdef class AlphaComplex: read and overwritten by the points in the `off_file`. :type off_file: string + :param weights: A list of weights. If set, the number of weights must correspond to the + number of points. + :type weights: list of double + + :param weight_file: A file containing a list of weights (one per line). + `weights` are read and overwritten by the weights in the `weight_file`. + If set, the number of weights must correspond to the number of points. + :type weight_file: string + :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is 'safe'. :type precision: string + + :raises FileNotFoundError: If `off_file` and/or `weight_file` is set but not found. + :raises ValueError: In case of inconsistency between the number of points and weights. """ # The real cython constructor - def __cinit__(self, points = [], off_file = '', precision = 'safe'): + def __cinit__(self, points = [], off_file = '', weights=[], weight_file='', precision = 'safe'): assert precision in ['fast', 'safe', 'exact'], "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' @@ -79,12 +92,28 @@ cdef class AlphaComplex: if os.path.isfile(off_file): points = read_points_from_off_file(off_file = off_file) else: - print("file " + off_file + " not found.") + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), off_file) + + if weight_file: + if os.path.isfile(weight_file): + weights = read_weights(weight_file = weight_file) + else: + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), weight_file) + # need to copy the points to use them without the gil cdef vector[vector[double]] pts + cdef vector[double] wgts pts = points - with nogil: - self.this_ptr = new Alpha_complex_interface(pts, fast, exact) + if len(weights) == 0: + with nogil: + self.this_ptr = new Alpha_complex_interface(pts, fast, exact) + else: + if len(weights) == len(points): + wgts = weights + with nogil: + self.this_ptr = new Alpha_complex_interface(pts, fast, exact) + else: + raise ValueError("Inconsistency between the number of points and weights") def __dealloc__(self): if self.this_ptr != NULL: diff --git a/src/python/gudhi/reader_utils.pyx b/src/python/gudhi/reader_utils.pyx index fe1c3a2e..f997ad3e 100644 --- a/src/python/gudhi/reader_utils.pyx +++ b/src/python/gudhi/reader_utils.pyx @@ -84,3 +84,19 @@ def read_persistence_intervals_in_dimension(persistence_file='', only_this_dim=- 'utf-8'), only_this_dim)) print("file " + persistence_file + " not set or not found.") return [] + +def read_weights(weight_file=''): + """Reads a file containing weights. Only one float value per line is read and stored. + The return value is a `list(weight)`. + + :param weight_file: A weight file style name (one weight per line). + :type weight_file: string + + :returns: A list of weights. + :rtype: List[float] + """ + weights=[] + with open(weight_file, 'r') as wfile: + weights = [float(wline) for wline in wfile if wline.strip()] + return weights + diff --git a/src/python/test/test_alpha_complex.py b/src/python/test/test_alpha_complex.py index 8f1424ec..35059339 100755 --- a/src/python/test/test_alpha_complex.py +++ b/src/python/test/test_alpha_complex.py @@ -261,3 +261,58 @@ def _3d_tetrahedrons(precision): def test_3d_tetrahedrons(): for precision in ['fast', 'safe', 'exact']: _3d_tetrahedrons(precision) + +def test_non_existing_off_file(): + with pytest.raises(FileNotFoundError): + alpha = gd.AlphaComplex(off_file="pouetpouettralala.toubiloubabdou") + +def test_non_existing_weight_file(): + off_file = open("alphacomplexdoc.off", "w") + off_file.write("OFF \n" \ + "7 0 0 \n" \ + "1.0 1.0 0.0\n" \ + "7.0 0.0 0.0\n" \ + "4.0 6.0 0.0\n" \ + "9.0 6.0 0.0\n" \ + "0.0 14.0 0.0\n" \ + "2.0 19.0 0.0\n" \ + "9.0 17.0 0.0\n" ) + off_file.close() + + with pytest.raises(FileNotFoundError): + alpha = gd.AlphaComplex(off_file="alphacomplexdoc.off", + weight_file="pouetpouettralala.toubiloubabdou") + + +def test_inconsistency_off_weight_file(): + off_file = open("alphacomplexdoc.off", "w") + off_file.write("OFF \n" \ + "7 0 0 \n" \ + "1.0 1.0 0.0\n" \ + "7.0 0.0 0.0\n" \ + "4.0 6.0 0.0\n" \ + "9.0 6.0 0.0\n" \ + "0.0 14.0 0.0\n" \ + "2.0 19.0 0.0\n" \ + "9.0 17.0 0.0\n" ) + off_file.close() + # 7 points, 8 weights, on purpose + weight_file = open("alphacomplexdoc.wgt", "w") + weight_file.write("5.0\n" \ + "2.0\n" \ + "7.0\n" \ + "4.0\n" \ + "9.0\n" \ + "0.0\n" \ + "2.0\n" \ + "9.0\n" ) + weight_file.close() + + with pytest.raises(ValueError): + alpha = gd.AlphaComplex(off_file="alphacomplexdoc.off", + weight_file="alphacomplexdoc.wgt") + + # 7 points, 6 weights, on purpose + with pytest.raises(ValueError): + alpha = gd.AlphaComplex(off_file="alphacomplexdoc.off", + weights=[1., 2., 3., 4., 5., 6.]) diff --git a/src/python/test/test_reader_utils.py b/src/python/test/test_reader_utils.py index 90da6651..91de9ba0 100755 --- a/src/python/test/test_reader_utils.py +++ b/src/python/test/test_reader_utils.py @@ -8,8 +8,9 @@ - YYYY/MM Author: Description of the modification """ -import gudhi +import gudhi as gd import numpy as np +from pytest import raises __author__ = "Vincent Rouvreau" __copyright__ = "Copyright (C) 2017 Inria" @@ -18,7 +19,7 @@ __license__ = "MIT" def test_non_existing_csv_file(): # Try to open a non existing file - matrix = gudhi.read_lower_triangular_matrix_from_csv_file( + matrix = gd.read_lower_triangular_matrix_from_csv_file( csv_file="pouetpouettralala.toubiloubabdou" ) assert matrix == [] @@ -29,7 +30,7 @@ def test_full_square_distance_matrix_csv_file(): test_file = open("full_square_distance_matrix.csv", "w") 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( + matrix = gd.read_lower_triangular_matrix_from_csv_file( csv_file="full_square_distance_matrix.csv" ) assert matrix == [[], [1.0], [2.0, 4.0], [3.0, 5.0, 6.0]] @@ -40,7 +41,7 @@ def test_lower_triangular_distance_matrix_csv_file(): test_file = open("lower_triangular_distance_matrix.csv", "w") test_file.write("\n1,\n2,3,\n4,5,6,\n7,8,9,10,") test_file.close() - matrix = gudhi.read_lower_triangular_matrix_from_csv_file( + matrix = gd.read_lower_triangular_matrix_from_csv_file( csv_file="lower_triangular_distance_matrix.csv", separator="," ) assert matrix == [[], [1.0], [2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0, 10.0]] @@ -48,11 +49,11 @@ def test_lower_triangular_distance_matrix_csv_file(): def test_non_existing_persistence_file(): # Try to open a non existing file - persistence = gudhi.read_persistence_intervals_grouped_by_dimension( + persistence = gd.read_persistence_intervals_grouped_by_dimension( persistence_file="pouetpouettralala.toubiloubabdou" ) assert persistence == [] - persistence = gudhi.read_persistence_intervals_in_dimension( + persistence = gd.read_persistence_intervals_in_dimension( persistence_file="pouetpouettralala.toubiloubabdou", only_this_dim=1 ) np.testing.assert_array_equal(persistence, []) @@ -65,21 +66,21 @@ def test_read_persistence_intervals_without_dimension(): "# Simple persistence diagram without dimension\n2.7 3.7\n9.6 14.\n34.2 34.974\n3. inf" ) test_file.close() - persistence = gudhi.read_persistence_intervals_in_dimension( + persistence = gd.read_persistence_intervals_in_dimension( persistence_file="persistence_intervals_without_dimension.pers" ) np.testing.assert_array_equal( persistence, [(2.7, 3.7), (9.6, 14.0), (34.2, 34.974), (3.0, float("Inf"))] ) - persistence = gudhi.read_persistence_intervals_in_dimension( + persistence = gd.read_persistence_intervals_in_dimension( persistence_file="persistence_intervals_without_dimension.pers", only_this_dim=0 ) np.testing.assert_array_equal(persistence, []) - persistence = gudhi.read_persistence_intervals_in_dimension( + persistence = gd.read_persistence_intervals_in_dimension( persistence_file="persistence_intervals_without_dimension.pers", only_this_dim=1 ) np.testing.assert_array_equal(persistence, []) - persistence = gudhi.read_persistence_intervals_grouped_by_dimension( + persistence = gd.read_persistence_intervals_grouped_by_dimension( persistence_file="persistence_intervals_without_dimension.pers" ) assert persistence == { @@ -94,29 +95,29 @@ def test_read_persistence_intervals_with_dimension(): "# Simple persistence diagram with dimension\n0 2.7 3.7\n1 9.6 14.\n3 34.2 34.974\n1 3. inf" ) test_file.close() - persistence = gudhi.read_persistence_intervals_in_dimension( + persistence = gd.read_persistence_intervals_in_dimension( persistence_file="persistence_intervals_with_dimension.pers" ) np.testing.assert_array_equal( persistence, [(2.7, 3.7), (9.6, 14.0), (34.2, 34.974), (3.0, float("Inf"))] ) - persistence = gudhi.read_persistence_intervals_in_dimension( + persistence = gd.read_persistence_intervals_in_dimension( persistence_file="persistence_intervals_with_dimension.pers", only_this_dim=0 ) np.testing.assert_array_equal(persistence, [(2.7, 3.7)]) - persistence = gudhi.read_persistence_intervals_in_dimension( + persistence = gd.read_persistence_intervals_in_dimension( persistence_file="persistence_intervals_with_dimension.pers", only_this_dim=1 ) np.testing.assert_array_equal(persistence, [(9.6, 14.0), (3.0, float("Inf"))]) - persistence = gudhi.read_persistence_intervals_in_dimension( + persistence = gd.read_persistence_intervals_in_dimension( persistence_file="persistence_intervals_with_dimension.pers", only_this_dim=2 ) np.testing.assert_array_equal(persistence, []) - persistence = gudhi.read_persistence_intervals_in_dimension( + persistence = gd.read_persistence_intervals_in_dimension( persistence_file="persistence_intervals_with_dimension.pers", only_this_dim=3 ) np.testing.assert_array_equal(persistence, [(34.2, 34.974)]) - persistence = gudhi.read_persistence_intervals_grouped_by_dimension( + persistence = gd.read_persistence_intervals_grouped_by_dimension( persistence_file="persistence_intervals_with_dimension.pers" ) assert persistence == { @@ -124,3 +125,19 @@ def test_read_persistence_intervals_with_dimension(): 1: [(9.6, 14.0), (3.0, float("Inf"))], 3: [(34.2, 34.974)], } + + +def test_non_existing_weights_file(): + with raises(FileNotFoundError): + # Try to open a non existing file + persistence = gd.read_weights(weight_file="pouetpouettralala.toubiloubabdou") + +def test_read_weights(): + # Create test file + test_file = open("test_read_weights.wgt", "w") + test_file.write( + "2.7\n 9.6 \n\t34.2\n3.\t\n\n" + ) + test_file.close() + weights = gd.read_weights(weight_file = "test_read_weights.wgt") + assert weights == [2.7, 9.6, 34.2, 3.] -- cgit v1.2.3 From af98f16e12ec9d1af7d925ecdc53b4daefea6ebe Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 18 Mar 2021 16:16:48 +0100 Subject: Add weight support --- src/python/doc/alpha_complex_user.rst | 104 ++++++++++++++++++++++----- src/python/gudhi/alpha_complex.pyx | 48 ++++++++----- src/python/include/Alpha_complex_factory.h | 65 +++++++++++++++-- src/python/include/Alpha_complex_interface.h | 28 +++++--- 4 files changed, 194 insertions(+), 51 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/doc/alpha_complex_user.rst b/src/python/doc/alpha_complex_user.rst index b116944c..6f35cc15 100644 --- a/src/python/doc/alpha_complex_user.rst +++ b/src/python/doc/alpha_complex_user.rst @@ -44,16 +44,15 @@ This example builds the alpha-complex from the given points: .. testcode:: - import gudhi - alpha_complex = gudhi.AlphaComplex(points=[[1, 1], [7, 0], [4, 6], [9, 6], [0, 14], [2, 19], [9, 17]]) - - 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) + from gudhi import AlphaComplex + ac = AlphaComplex(points=[[1, 1], [7, 0], [4, 6], [9, 6], [0, 14], [2, 19], [9, 17]]) + + stree = ac.create_simplex_tree() + print('Alpha complex is of dimension ', stree.dimension(), ' - ', + stree.num_simplices(), ' simplices - ', stree.num_vertices(), ' vertices.') + fmt = '%s -> %.2f' - for filtered_value in simplex_tree.get_filtration(): + for filtered_value in stree.get_filtration(): print(fmt % tuple(filtered_value)) The output is: @@ -174,6 +173,78 @@ of speed-up, since we always first build the full filtered complex, so it is rec :paramref:`~gudhi.AlphaComplex.create_simplex_tree.max_alpha_square`. In the following example, a threshold of :math:`\alpha^2 = 32.0` is used. +Weighted specific version +^^^^^^^^^^^^^^^^^^^^^^^^^ + +:Requires: `Eigen `_ :math:`\geq` 3.1.0 and `CGAL `_ :math:`\geq` 5.1.0. + +A weighted version for Alpha complex is available. It is like a usual Alpha complex, but based on a +`CGAL regular triangulation `_ +of Delaunay. + +This example builds the CGAL weighted alpha shapes from a small molecule, and initializes the alpha complex with +it. This example is taken from +`CGAL 3d weighted alpha shapes `_. + +Then, it is asked to display information about the alpha complex. + +.. testcode:: + + from gudhi import AlphaComplex + wgt_ac = AlphaComplex(weighted_points=[[[ 1., -1., -1.], 4.], + [[-1., 1., -1.], 4.], + [[-1., -1., 1.], 4.], + [[ 1., 1., 1.], 4.], + [[ 2., 2., 2.], 1.]]) + # equivalent to: + # wgt_ac = AlphaComplex(points=[[ 1., -1., -1.], + # [-1., 1., -1.], + # [-1., -1., 1.], + # [ 1., 1., 1.], + # [ 2., 2., 2.]], + # weights = [4., 4., 4., 4., 1.]) + + stree = wgt_ac.create_simplex_tree() + print('Weighted alpha complex is of dimension ', stree.dimension(), ' - ', + stree.num_simplices(), ' simplices - ', stree.num_vertices(), ' vertices.') + fmt = '%s -> %.2f' + for filtered_value in stree.get_filtration(): + print(fmt % tuple(filtered_value)) + +The output is: + +.. testoutput:: + + Weighted alpha complex is of dimension 3 - 29 simplices - 5 vertices. + [ 0 ] -> [-4] + [ 1 ] -> [-4] + [ 2 ] -> [-4] + [ 3 ] -> [-4] + [ 1, 0 ] -> [-2] + [ 2, 0 ] -> [-2] + [ 2, 1 ] -> [-2] + [ 3, 0 ] -> [-2] + [ 3, 1 ] -> [-2] + [ 3, 2 ] -> [-2] + [ 2, 1, 0 ] -> [-1.33333] + [ 3, 1, 0 ] -> [-1.33333] + [ 3, 2, 0 ] -> [-1.33333] + [ 3, 2, 1 ] -> [-1.33333] + [ 3, 2, 1, 0 ] -> [-1] + [ 4 ] -> [-1] + [ 4, 2 ] -> [-1] + [ 4, 0 ] -> [23] + [ 4, 1 ] -> [23] + [ 4, 2, 0 ] -> [23] + [ 4, 2, 1 ] -> [23] + [ 4, 3 ] -> [23] + [ 4, 3, 2 ] -> [23] + [ 4, 1, 0 ] -> [95] + [ 4, 2, 1, 0 ] -> [95] + [ 4, 3, 0 ] -> [95] + [ 4, 3, 1 ] -> [95] + [ 4, 3, 2, 0 ] -> [95] + [ 4, 3, 2, 1 ] -> [95] Example from OFF file ^^^^^^^^^^^^^^^^^^^^^ @@ -186,14 +257,9 @@ Then, it computes the persistence diagram and displays it: :include-source: import matplotlib.pyplot as plt - import gudhi - alpha_complex = gudhi.AlphaComplex(off_file=gudhi.__root_source_dir__ + \ - '/data/points/tore3D_300.off') - 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) - diag = simplex_tree.persistence() - gudhi.plot_persistence_diagram(diag) + import gudhi as gd + ac = gd.AlphaComplex(off_file=gd.__root_source_dir__ + '/data/points/tore3D_300.off') + stree = ac.create_simplex_tree() + dgm = stree.persistence() + gd.plot_persistence_diagram(dgm, legend = True) plt.show() diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index b7c20f74..681faebe 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -29,7 +29,7 @@ __license__ = "GPL v3" cdef extern from "Alpha_complex_interface.h" namespace "Gudhi": cdef cppclass Alpha_complex_interface "Gudhi::alpha_complex::Alpha_complex_interface": - Alpha_complex_interface(vector[vector[double]] points, bool fast_version, bool exact_version) nogil except + + Alpha_complex_interface(vector[vector[double]] points, vector[double] weights, bool fast_version, bool exact_version) nogil except + vector[double] get_point(int vertex) nogil except + void create_simplex_tree(Simplex_tree_interface_full_featured* simplex_tree, double max_alpha_square, bool default_filtration_value) nogil except + @@ -56,26 +56,35 @@ cdef class AlphaComplex: cdef Alpha_complex_interface * this_ptr # Fake constructor that does nothing but documenting the constructor - def __init__(self, points=[], off_file='', weights=[], weight_file='', precision='safe'): + def __init__(self, points=[], off_file='', weights=[], weight_file='', weighted_points=[], + precision='safe'): """AlphaComplex constructor. :param points: A list of points in d-Dimension. - :type points: list of list of double + :type points: Iterable[Iterable[float]] - :param off_file: An `OFF file style `_ name. `points` are - read and overwritten by the points in the `off_file`. + :param off_file: An `OFF file style `_ name. + If an `off_file` is given with `points` or `weighted_points`, only points from the + file are taken into account. :type off_file: string :param weights: A list of weights. If set, the number of weights must correspond to the number of points. - :type weights: list of double + :type weights: Iterable[float] :param weight_file: A file containing a list of weights (one per line). - `weights` are read and overwritten by the weights in the `weight_file`. - If set, the number of weights must correspond to the number of points. + If a `weight_file` is given with `weights` or `weighted_points`, only weights from the + file are taken into account. + :type weight_file: string - :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is 'safe'. + :param weighted_points: A list of points in d-Dimension and its weight. + If `weighted_points` are given with `weights` or `points`, these last ones will + not be taken into account. + :type weighted_points: Iterable[Iterable[float], float] + + :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is + 'safe'. :type precision: string :raises FileNotFoundError: If `off_file` and/or `weight_file` is set but not found. @@ -83,11 +92,16 @@ cdef class AlphaComplex: """ # The real cython constructor - def __cinit__(self, points = [], off_file = '', weights=[], weight_file='', precision = 'safe'): + def __cinit__(self, points = [], off_file = '', weights=[], weight_file='', weighted_points=[], + precision = 'safe'): assert precision in ['fast', 'safe', 'exact'], "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' + if len(weighted_points) > 0: + points = [wpt[0] for wpt in weighted_points] + weights = [wpt[1] for wpt in weighted_points] + if off_file: if os.path.isfile(off_file): points = read_points_from_off_file(off_file = off_file) @@ -100,20 +114,18 @@ cdef class AlphaComplex: else: raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), weight_file) + # weights are set but is inconsistent with the number of points + if len(weights) != 0 and len(weights) != len(points): + raise ValueError("Inconsistency between the number of points and weights") + # need to copy the points to use them without the gil cdef vector[vector[double]] pts cdef vector[double] wgts pts = points + wgts = weights if len(weights) == 0: with nogil: - self.this_ptr = new Alpha_complex_interface(pts, fast, exact) - else: - if len(weights) == len(points): - wgts = weights - with nogil: - self.this_ptr = new Alpha_complex_interface(pts, fast, exact) - else: - raise ValueError("Inconsistency between the number of points and weights") + self.this_ptr = new Alpha_complex_interface(pts, wgts, fast, exact) def __dealloc__(self): if self.this_ptr != NULL: diff --git a/src/python/include/Alpha_complex_factory.h b/src/python/include/Alpha_complex_factory.h index 3405fdd6..36e98615 100644 --- a/src/python/include/Alpha_complex_factory.h +++ b/src/python/include/Alpha_complex_factory.h @@ -55,13 +55,13 @@ class Abstract_alpha_complex { virtual ~Abstract_alpha_complex() = default; }; -class Exact_Alphacomplex_dD final : public Abstract_alpha_complex { +class Exact_alpha_complex_dD final : public Abstract_alpha_complex { private: using Kernel = CGAL::Epeck_d; using Point = typename Kernel::Point_d; public: - Exact_Alphacomplex_dD(const std::vector>& points, bool exact_version) + Exact_alpha_complex_dD(const std::vector>& points, bool exact_version) : exact_version_(exact_version), alpha_complex_(boost::adaptors::transform(points, pt_cython_to_cgal)) { } @@ -81,13 +81,13 @@ class Exact_Alphacomplex_dD final : public Abstract_alpha_complex { Alpha_complex alpha_complex_; }; -class Inexact_Alphacomplex_dD final : public Abstract_alpha_complex { +class Inexact_alpha_complex_dD final : public Abstract_alpha_complex { private: using Kernel = CGAL::Epick_d; using Point = typename Kernel::Point_d; public: - Inexact_Alphacomplex_dD(const std::vector>& points, bool exact_version) + Inexact_alpha_complex_dD(const std::vector>& points, bool exact_version) : exact_version_(exact_version), alpha_complex_(boost::adaptors::transform(points, pt_cython_to_cgal)) { } @@ -106,8 +106,61 @@ class Inexact_Alphacomplex_dD final : public Abstract_alpha_complex { Alpha_complex alpha_complex_; }; +class Exact_weighted_alpha_complex_dD final : public Abstract_alpha_complex { + private: + using Kernel = CGAL::Epeck_d; + using Point = typename Kernel::Point_d; + + public: + Exact_weighted_alpha_complex_dD(const std::vector>& points, + const std::vector& weights, bool exact_version) + : exact_version_(exact_version), + alpha_complex_(boost::adaptors::transform(points, pt_cython_to_cgal), weights) { + } + + virtual std::vector get_point(int vh) override { + Point const& point = alpha_complex_.get_point(vh).point(); + return pt_cgal_to_cython(point); + } + + virtual bool create_simplex_tree(Simplex_tree_interface<>* simplex_tree, double max_alpha_square, + bool default_filtration_value) override { + return alpha_complex_.create_complex(*simplex_tree, max_alpha_square, exact_version_, default_filtration_value); + } + + private: + bool exact_version_; + Alpha_complex alpha_complex_; +}; + +class Inexact_weighted_alpha_complex_dD final : public Abstract_alpha_complex { + private: + using Kernel = CGAL::Epick_d; + using Point = typename Kernel::Point_d; + + public: + Inexact_weighted_alpha_complex_dD(const std::vector>& points, + const std::vector& weights, bool exact_version) + : exact_version_(exact_version), + alpha_complex_(boost::adaptors::transform(points, pt_cython_to_cgal), weights) { + } + + virtual std::vector get_point(int vh) override { + Point const& point = alpha_complex_.get_point(vh).point(); + return pt_cgal_to_cython(point); + } + virtual bool create_simplex_tree(Simplex_tree_interface<>* simplex_tree, double max_alpha_square, + bool default_filtration_value) override { + return alpha_complex_.create_complex(*simplex_tree, max_alpha_square, exact_version_, default_filtration_value); + } + + private: + bool exact_version_; + Alpha_complex alpha_complex_; +}; + template -class Alphacomplex_3D final : public Abstract_alpha_complex { +class Alpha_complex_3D final : public Abstract_alpha_complex { private: using Point = typename Alpha_complex_3d::Bare_point_3; @@ -116,7 +169,7 @@ class Alphacomplex_3D final : public Abstract_alpha_complex { } public: - Alphacomplex_3D(const std::vector>& points) + Alpha_complex_3D(const std::vector>& points) : alpha_complex_(boost::adaptors::transform(points, pt_cython_to_cgal_3)) { } diff --git a/src/python/include/Alpha_complex_interface.h b/src/python/include/Alpha_complex_interface.h index 23be194d..43c96b2f 100644 --- a/src/python/include/Alpha_complex_interface.h +++ b/src/python/include/Alpha_complex_interface.h @@ -27,8 +27,11 @@ namespace alpha_complex { class Alpha_complex_interface { public: - Alpha_complex_interface(const std::vector>& points, bool fast_version, bool exact_version) + Alpha_complex_interface(const std::vector>& points, + const std::vector& weights, + bool fast_version, bool exact_version) : points_(points), + weights_(weights), fast_version_(fast_version), exact_version_(exact_version) { } @@ -41,13 +44,13 @@ class Alpha_complex_interface { bool default_filtration_value) { if (points_.size() > 0) { std::size_t dimension = points_[0].size(); - if (dimension == 3 && !default_filtration_value) { + if (dimension == 3 && weights_.size() == 0 && !default_filtration_value) { if (fast_version_) - alpha_ptr_ = std::make_unique>(points_); + alpha_ptr_ = std::make_unique>(points_); else if (exact_version_) - alpha_ptr_ = std::make_unique>(points_); + alpha_ptr_ = std::make_unique>(points_); else - alpha_ptr_ = std::make_unique>(points_); + alpha_ptr_ = std::make_unique>(points_); if (!alpha_ptr_->create_simplex_tree(simplex_tree, max_alpha_square, default_filtration_value)) { // create_simplex_tree will fail if all points are on a plane - Retry with dD by setting dimension to 2 dimension--; @@ -55,11 +58,19 @@ class Alpha_complex_interface { } } // Not ** else ** because we have to take into account if 3d fails - if (dimension != 3 || default_filtration_value) { + if (dimension != 3 || weights_.size() != 0 || default_filtration_value) { if (fast_version_) { - alpha_ptr_ = std::make_unique(points_, exact_version_); + if (weights_.size() == 0) { + alpha_ptr_ = std::make_unique(points_, exact_version_); + } else { + alpha_ptr_ = std::make_unique(points_, weights_, exact_version_); + } } else { - alpha_ptr_ = std::make_unique(points_, exact_version_); + if (weights_.size() == 0) { + alpha_ptr_ = std::make_unique(points_, exact_version_); + } else { + alpha_ptr_ = std::make_unique(points_, weights_, exact_version_); + } } alpha_ptr_->create_simplex_tree(simplex_tree, max_alpha_square, default_filtration_value); } @@ -69,6 +80,7 @@ class Alpha_complex_interface { private: std::unique_ptr alpha_ptr_; std::vector> points_; + std::vector weights_; bool fast_version_; bool exact_version_; }; -- cgit v1.2.3 From 0fc53e3b820130169a6a9ad2866cff6f1b4a64c6 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Mon, 22 Mar 2021 16:25:58 +0100 Subject: Fix sphinx issues --- src/python/doc/alpha_complex_user.rst | 62 +++++++++++++++++------------------ src/python/gudhi/alpha_complex.pyx | 5 ++- 2 files changed, 33 insertions(+), 34 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/doc/alpha_complex_user.rst b/src/python/doc/alpha_complex_user.rst index 6f35cc15..5e028cdc 100644 --- a/src/python/doc/alpha_complex_user.rst +++ b/src/python/doc/alpha_complex_user.rst @@ -59,7 +59,7 @@ The output is: .. testoutput:: - Alpha complex is of dimension 2 - 25 simplices - 7 vertices. + Alpha complex is of dimension 2 - 25 simplices - 7 vertices. [0] -> 0.00 [1] -> 0.00 [2] -> 0.00 @@ -215,36 +215,36 @@ The output is: .. testoutput:: - Weighted alpha complex is of dimension 3 - 29 simplices - 5 vertices. - [ 0 ] -> [-4] - [ 1 ] -> [-4] - [ 2 ] -> [-4] - [ 3 ] -> [-4] - [ 1, 0 ] -> [-2] - [ 2, 0 ] -> [-2] - [ 2, 1 ] -> [-2] - [ 3, 0 ] -> [-2] - [ 3, 1 ] -> [-2] - [ 3, 2 ] -> [-2] - [ 2, 1, 0 ] -> [-1.33333] - [ 3, 1, 0 ] -> [-1.33333] - [ 3, 2, 0 ] -> [-1.33333] - [ 3, 2, 1 ] -> [-1.33333] - [ 3, 2, 1, 0 ] -> [-1] - [ 4 ] -> [-1] - [ 4, 2 ] -> [-1] - [ 4, 0 ] -> [23] - [ 4, 1 ] -> [23] - [ 4, 2, 0 ] -> [23] - [ 4, 2, 1 ] -> [23] - [ 4, 3 ] -> [23] - [ 4, 3, 2 ] -> [23] - [ 4, 1, 0 ] -> [95] - [ 4, 2, 1, 0 ] -> [95] - [ 4, 3, 0 ] -> [95] - [ 4, 3, 1 ] -> [95] - [ 4, 3, 2, 0 ] -> [95] - [ 4, 3, 2, 1 ] -> [95] + Weighted alpha complex is of dimension 3 - 29 simplices - 5 vertices. + [0] -> -4.00 + [1] -> -4.00 + [2] -> -4.00 + [3] -> -4.00 + [0, 1] -> -2.00 + [0, 2] -> -2.00 + [1, 2] -> -2.00 + [0, 3] -> -2.00 + [1, 3] -> -2.00 + [2, 3] -> -2.00 + [0, 2, 3] -> -1.33 + [1, 2, 3] -> -1.33 + [0, 1, 2] -> -1.33 + [0, 1, 3] -> -1.33 + [0, 1, 2, 3] -> -1.00 + [4] -> -1.00 + [3, 4] -> -1.00 + [0, 4] -> 23.00 + [1, 4] -> 23.00 + [2, 4] -> 23.00 + [0, 3, 4] -> 23.00 + [1, 3, 4] -> 23.00 + [2, 3, 4] -> 23.00 + [0, 1, 4] -> 95.00 + [0, 2, 4] -> 95.00 + [1, 2, 4] -> 95.00 + [0, 1, 3, 4] -> 95.00 + [0, 2, 3, 4] -> 95.00 + [1, 2, 3, 4] -> 95.00 Example from OFF file ^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index 681faebe..d4c4ba20 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -123,9 +123,8 @@ cdef class AlphaComplex: cdef vector[double] wgts pts = points wgts = weights - if len(weights) == 0: - with nogil: - self.this_ptr = new Alpha_complex_interface(pts, wgts, fast, exact) + with nogil: + self.this_ptr = new Alpha_complex_interface(pts, wgts, fast, exact) def __dealloc__(self): if self.this_ptr != NULL: -- cgit v1.2.3 From 0313c98f32363bfc75162613b3cfa9b7efa4081b Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 23 Mar 2021 16:10:24 +0100 Subject: Add simplex tree equality operator to be able to test alpha complex --- src/python/gudhi/simplex_tree.pxd | 1 + src/python/gudhi/simplex_tree.pyx | 9 +++++++++ src/python/test/test_alpha_complex.py | 37 +++++++++++++++++++++++++++++++++++ src/python/test/test_simplex_tree.py | 12 ++++++++++++ 4 files changed, 59 insertions(+) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index 000323af..2aa435b1 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -74,6 +74,7 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": Simplex_tree_skeleton_iterator get_skeleton_iterator_begin(int dimension) nogil Simplex_tree_skeleton_iterator get_skeleton_iterator_end(int dimension) nogil pair[Simplex_tree_boundary_iterator, Simplex_tree_boundary_iterator] get_boundary_iterators(vector[int] simplex) nogil except + + bint operator==(Simplex_tree_interface_full_featured) nogil cdef extern from "Persistent_cohomology_interface.h" namespace "Gudhi": cdef cppclass Simplex_tree_persistence_interface "Gudhi::Persistent_cohomology_interface>": diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index d7991417..b5a938d5 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -639,3 +639,12 @@ cdef class SimplexTree: self.thisptr = (ptr.collapse_edges(nb_iter)) # Delete old pointer del ptr + + def __eq__(self, other): + """Simplex tree equality operator using C++ depth first search operator== + + :returns: True if the 2 simplex trees are equal, False otherwise. + :rtype: bool + """ + cdef intptr_t other_int_ptr=other.thisptr + return dereference(self.get_ptr()) == dereference(other_int_ptr) \ No newline at end of file diff --git a/src/python/test/test_alpha_complex.py b/src/python/test/test_alpha_complex.py index 35059339..a0de46c3 100755 --- a/src/python/test/test_alpha_complex.py +++ b/src/python/test/test_alpha_complex.py @@ -316,3 +316,40 @@ def test_inconsistency_off_weight_file(): with pytest.raises(ValueError): alpha = gd.AlphaComplex(off_file="alphacomplexdoc.off", weights=[1., 2., 3., 4., 5., 6.]) + +def _with_or_without_weight_file(precision): + off_file = open("weightalphacomplex.off", "w") + off_file.write("OFF \n" \ + "5 0 0 \n" \ + "1. -1. -1. \n" \ + "-1. 1. -1. \n" \ + "-1. -1. 1. \n" \ + "1. 1. 1. \n" \ + "2. 2. 2.") + off_file.close() + + weight_file = open("weightalphacomplex.wgt", "w") + weight_file.write("4.0\n" \ + "4.0\n" \ + "4.0\n" \ + "4.0\n" \ + "1.0\n" ) + weight_file.close() + + stree_from_files = gd.AlphaComplex(off_file="weightalphacomplex.off", + weight_file="weightalphacomplex.wgt", + precision = precision).create_simplex_tree() + + stree_from_values = gd.AlphaComplex(points=[[ 1., -1., -1.], + [-1., 1., -1.], + [-1., -1., 1.], + [ 1., 1., 1.], + [ 2., 2., 2.]], + weights = [4., 4., 4., 4., 1.], + precision = precision).create_simplex_tree() + + assert stree_from_files == stree_from_values + +def test_with_or_without_weight_file(): + for precision in ['fast', 'safe', 'exact']: + _with_or_without_weight_file(precision) diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index a3eacaa9..83b5c268 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -404,3 +404,15 @@ def test_boundaries_iterator(): with pytest.raises(RuntimeError): list(st.get_boundaries([6])) # (6) does not exist + +def test_equality_operator(): + st1 = SimplexTree() + st2 = SimplexTree() + + assert st1 == st2 + + st1.insert([1,2,3], 4.) + assert st1 != st2 + + st2.insert([1,2,3], 4.) + assert st1 == st2 -- cgit v1.2.3 From dcb90cc644f91265c9bd3011a89b5f0b0e9f3869 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 23 Mar 2021 16:21:00 +0100 Subject: Remove weighted_points, as it is overdesigned --- src/python/doc/alpha_complex_user.rst | 18 ++++++------------ src/python/gudhi/alpha_complex.pyx | 22 ++++++---------------- 2 files changed, 12 insertions(+), 28 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/doc/alpha_complex_user.rst b/src/python/doc/alpha_complex_user.rst index 5e028cdc..f59f69e7 100644 --- a/src/python/doc/alpha_complex_user.rst +++ b/src/python/doc/alpha_complex_user.rst @@ -191,18 +191,12 @@ Then, it is asked to display information about the alpha complex. .. testcode:: from gudhi import AlphaComplex - wgt_ac = AlphaComplex(weighted_points=[[[ 1., -1., -1.], 4.], - [[-1., 1., -1.], 4.], - [[-1., -1., 1.], 4.], - [[ 1., 1., 1.], 4.], - [[ 2., 2., 2.], 1.]]) - # equivalent to: - # wgt_ac = AlphaComplex(points=[[ 1., -1., -1.], - # [-1., 1., -1.], - # [-1., -1., 1.], - # [ 1., 1., 1.], - # [ 2., 2., 2.]], - # weights = [4., 4., 4., 4., 1.]) + wgt_ac = AlphaComplex(points=[[ 1., -1., -1.], + [-1., 1., -1.], + [-1., -1., 1.], + [ 1., 1., 1.], + [ 2., 2., 2.]], + weights = [4., 4., 4., 4., 1.]) stree = wgt_ac.create_simplex_tree() print('Weighted alpha complex is of dimension ', stree.dimension(), ' - ', diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index d4c4ba20..9c364b76 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -56,15 +56,14 @@ cdef class AlphaComplex: cdef Alpha_complex_interface * this_ptr # Fake constructor that does nothing but documenting the constructor - def __init__(self, points=[], off_file='', weights=[], weight_file='', weighted_points=[], - precision='safe'): + def __init__(self, points=[], off_file='', weights=[], weight_file='', precision='safe'): """AlphaComplex constructor. :param points: A list of points in d-Dimension. :type points: Iterable[Iterable[float]] :param off_file: An `OFF file style `_ name. - If an `off_file` is given with `points` or `weighted_points`, only points from the + If an `off_file` is given with `points` as arguments, only points from the file are taken into account. :type off_file: string @@ -73,16 +72,11 @@ cdef class AlphaComplex: :type weights: Iterable[float] :param weight_file: A file containing a list of weights (one per line). - If a `weight_file` is given with `weights` or `weighted_points`, only weights from the + If a `weight_file` is given with `weights` as arguments, only weights from the file are taken into account. :type weight_file: string - :param weighted_points: A list of points in d-Dimension and its weight. - If `weighted_points` are given with `weights` or `points`, these last ones will - not be taken into account. - :type weighted_points: Iterable[Iterable[float], float] - :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is 'safe'. :type precision: string @@ -92,16 +86,12 @@ cdef class AlphaComplex: """ # The real cython constructor - def __cinit__(self, points = [], off_file = '', weights=[], weight_file='', weighted_points=[], - precision = 'safe'): - assert precision in ['fast', 'safe', 'exact'], "Alpha complex precision can only be 'fast', 'safe' or 'exact'" + def __cinit__(self, points = [], off_file = '', weights=[], weight_file='', precision = 'safe'): + assert precision in ['fast', 'safe', 'exact'], \ + "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' - if len(weighted_points) > 0: - points = [wpt[0] for wpt in weighted_points] - weights = [wpt[1] for wpt in weighted_points] - if off_file: if os.path.isfile(off_file): points = read_points_from_off_file(off_file = off_file) -- cgit v1.2.3 From 77e577eb28ca7622553cd0527db76d46b473c445 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Wed, 24 Mar 2021 15:27:11 +0100 Subject: Remove read_weights and depreciate off_file --- src/python/doc/alpha_complex_user.rst | 8 +- ...ex_diagram_persistence_from_off_file_example.py | 55 ++++------ .../alpha_rips_persistence_bottleneck_distance.py | 110 +++++++++---------- src/python/example/plot_alpha_complex.py | 5 +- src/python/gudhi/alpha_complex.pyx | 30 ++---- src/python/gudhi/reader_utils.pyx | 16 --- src/python/test/test_alpha_complex.py | 116 +++++++-------------- src/python/test/test_reader_utils.py | 16 --- 8 files changed, 128 insertions(+), 228 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/doc/alpha_complex_user.rst b/src/python/doc/alpha_complex_user.rst index f59f69e7..2b4b75cf 100644 --- a/src/python/doc/alpha_complex_user.rst +++ b/src/python/doc/alpha_complex_user.rst @@ -243,7 +243,8 @@ The output is: Example from OFF file ^^^^^^^^^^^^^^^^^^^^^ -This example builds the alpha complex from 300 random points on a 2-torus, given by an `OFF file `_. +This example builds the alpha complex from 300 random points on a 2-torus, given by an +`OFF file `_. Then, it computes the persistence diagram and displays it: @@ -252,8 +253,9 @@ Then, it computes the persistence diagram and displays it: import matplotlib.pyplot as plt import gudhi as gd - ac = gd.AlphaComplex(off_file=gd.__root_source_dir__ + '/data/points/tore3D_300.off') - stree = ac.create_simplex_tree() + off_file = gd.__root_source_dir__ + '/data/points/tore3D_300.off' + points = gd.read_points_from_off_file(off_file = off_file) + stree = gd.AlphaComplex(points = points).create_simplex_tree() dgm = stree.persistence() gd.plot_persistence_diagram(dgm, legend = True) plt.show() diff --git a/src/python/example/alpha_complex_diagram_persistence_from_off_file_example.py b/src/python/example/alpha_complex_diagram_persistence_from_off_file_example.py index fe03be31..c96121a6 100755 --- a/src/python/example/alpha_complex_diagram_persistence_from_off_file_example.py +++ b/src/python/example/alpha_complex_diagram_persistence_from_off_file_example.py @@ -1,9 +1,7 @@ #!/usr/bin/env python import argparse -import errno -import os -import gudhi +import gudhi as gd """ This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. @@ -41,33 +39,24 @@ parser.add_argument( args = parser.parse_args() -with open(args.file, "r") as f: - first_line = f.readline() - if (first_line == "OFF\n") or (first_line == "nOFF\n"): - print("##############################################################") - print("AlphaComplex creation from points read in a OFF file") - - alpha_complex = gudhi.AlphaComplex(off_file=args.file) - if args.max_alpha_square is not None: - print("with max_edge_length=", args.max_alpha_square) - simplex_tree = alpha_complex.create_simplex_tree( - max_alpha_square=args.max_alpha_square - ) - else: - simplex_tree = alpha_complex.create_simplex_tree() - - print("Number of simplices=", simplex_tree.num_simplices()) - - diag = simplex_tree.persistence() - - print("betti_numbers()=", simplex_tree.betti_numbers()) - - if args.no_diagram == False: - import matplotlib.pyplot as plot - gudhi.plot_persistence_diagram(diag, band=args.band) - plot.show() - else: - raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), - args.file) - - f.close() +print("##############################################################") +print("AlphaComplex creation from points read in a OFF file") + +points = gd.read_points_from_off_file(off_file = args.file) +alpha_complex = gd.AlphaComplex(points = points) +if args.max_alpha_square is not None: + print("with max_edge_length=", args.max_alpha_square) + simplex_tree = alpha_complex.create_simplex_tree( + max_alpha_square=args.max_alpha_square + ) +else: + simplex_tree = alpha_complex.create_simplex_tree() + +print("Number of simplices=", simplex_tree.num_simplices()) + +diag = simplex_tree.persistence() +print("betti_numbers()=", simplex_tree.betti_numbers()) +if args.no_diagram == False: + import matplotlib.pyplot as plot + gd.plot_persistence_diagram(diag, band=args.band) + plot.show() diff --git a/src/python/example/alpha_rips_persistence_bottleneck_distance.py b/src/python/example/alpha_rips_persistence_bottleneck_distance.py index 3e12b0d5..6b97fb3b 100755 --- a/src/python/example/alpha_rips_persistence_bottleneck_distance.py +++ b/src/python/example/alpha_rips_persistence_bottleneck_distance.py @@ -1,10 +1,8 @@ #!/usr/bin/env python -import gudhi +import gudhi as gd import argparse import math -import errno -import os import numpy as np """ This file is part of the Gudhi Library - https://gudhi.inria.fr/ - @@ -37,70 +35,60 @@ parser.add_argument("-t", "--threshold", type=float, default=0.5) parser.add_argument("-d", "--max_dimension", type=int, default=1) args = parser.parse_args() -with open(args.file, "r") as f: - first_line = f.readline() - if (first_line == "OFF\n") or (first_line == "nOFF\n"): - point_cloud = gudhi.read_points_from_off_file(off_file=args.file) - print("##############################################################") - print("RipsComplex creation from points read in a OFF file") +point_cloud = gd.read_points_from_off_file(off_file=args.file) +print("##############################################################") +print("RipsComplex creation from points read in a OFF file") - message = "RipsComplex with max_edge_length=" + repr(args.threshold) - print(message) +message = "RipsComplex with max_edge_length=" + repr(args.threshold) +print(message) - rips_complex = gudhi.RipsComplex( - points=point_cloud, max_edge_length=args.threshold - ) - - rips_stree = rips_complex.create_simplex_tree( - max_dimension=args.max_dimension) - - message = "Number of simplices=" + repr(rips_stree.num_simplices()) - print(message) - - rips_stree.compute_persistence() - - print("##############################################################") - print("AlphaComplex creation from points read in a OFF file") - - message = "AlphaComplex with max_edge_length=" + repr(args.threshold) - print(message) - - alpha_complex = gudhi.AlphaComplex(points=point_cloud) - alpha_stree = alpha_complex.create_simplex_tree( - max_alpha_square=(args.threshold * args.threshold) - ) - - message = "Number of simplices=" + repr(alpha_stree.num_simplices()) - print(message) +rips_complex = gd.RipsComplex( + points=point_cloud, max_edge_length=args.threshold +) - alpha_stree.compute_persistence() +rips_stree = rips_complex.create_simplex_tree( + max_dimension=args.max_dimension) - max_b_distance = 0.0 - for dim in range(args.max_dimension): - # Alpha persistence values needs to be transform because filtration - # values are alpha square values - alpha_intervals = np.sqrt(alpha_stree.persistence_intervals_in_dimension(dim)) +message = "Number of simplices=" + repr(rips_stree.num_simplices()) +print(message) - rips_intervals = rips_stree.persistence_intervals_in_dimension(dim) - bottleneck_distance = gudhi.bottleneck_distance( - rips_intervals, alpha_intervals - ) - message = ( - "In dimension " - + repr(dim) - + ", bottleneck distance = " - + repr(bottleneck_distance) - ) - print(message) - max_b_distance = max(bottleneck_distance, max_b_distance) +rips_stree.compute_persistence() - print("==============================================================") - message = "Bottleneck distance is " + repr(max_b_distance) - print(message) +print("##############################################################") +print("AlphaComplex creation from points read in a OFF file") - else: - raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), - args.file) +message = "AlphaComplex with max_edge_length=" + repr(args.threshold) +print(message) +alpha_complex = gd.AlphaComplex(points=point_cloud) +alpha_stree = alpha_complex.create_simplex_tree( + max_alpha_square=(args.threshold * args.threshold) +) - f.close() +message = "Number of simplices=" + repr(alpha_stree.num_simplices()) +print(message) + +alpha_stree.compute_persistence() + +max_b_distance = 0.0 +for dim in range(args.max_dimension): + # Alpha persistence values needs to be transform because filtration + # values are alpha square values + alpha_intervals = np.sqrt(alpha_stree.persistence_intervals_in_dimension(dim)) + + rips_intervals = rips_stree.persistence_intervals_in_dimension(dim) + bottleneck_distance = gd.bottleneck_distance( + rips_intervals, alpha_intervals + ) + message = ( + "In dimension " + + repr(dim) + + ", bottleneck distance = " + + repr(bottleneck_distance) + ) + print(message) + max_b_distance = max(bottleneck_distance, max_b_distance) + +print("==============================================================") +message = "Bottleneck distance is " + repr(max_b_distance) +print(message) diff --git a/src/python/example/plot_alpha_complex.py b/src/python/example/plot_alpha_complex.py index 99c18a7c..0924619b 100755 --- a/src/python/example/plot_alpha_complex.py +++ b/src/python/example/plot_alpha_complex.py @@ -1,8 +1,9 @@ #!/usr/bin/env python import numpy as np -import gudhi -ac = gudhi.AlphaComplex(off_file='../../data/points/tore3D_1307.off') +import gudhi as gd +points = gd.read_points_from_off_file(off_file = '../../data/points/tore3D_1307.off') +ac = gd.AlphaComplex(points = points) st = ac.create_simplex_tree() points = np.array([ac.get_point(i) for i in range(st.num_vertices())]) # We want to plot the alpha-complex with alpha=0.1. diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index 9c364b76..5d181391 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -18,10 +18,11 @@ from libcpp cimport bool from libc.stdint cimport intptr_t import errno import os +import warnings from gudhi.simplex_tree cimport * from gudhi.simplex_tree import SimplexTree -from gudhi import read_points_from_off_file, read_weights +from gudhi import read_points_from_off_file __author__ = "Vincent Rouvreau" __copyright__ = "Copyright (C) 2016 Inria" @@ -56,54 +57,45 @@ cdef class AlphaComplex: cdef Alpha_complex_interface * this_ptr # Fake constructor that does nothing but documenting the constructor - def __init__(self, points=[], off_file='', weights=[], weight_file='', precision='safe'): + def __init__(self, points=[], off_file='', weights=[], precision='safe'): """AlphaComplex constructor. :param points: A list of points in d-Dimension. :type points: Iterable[Iterable[float]] - :param off_file: An `OFF file style `_ name. - If an `off_file` is given with `points` as arguments, only points from the - file are taken into account. + :param off_file: **[deprecated]** An `OFF file style `_ + name. + If an `off_file` is given with `points` as arguments, only points from the file are + taken into account. :type off_file: string :param weights: A list of weights. If set, the number of weights must correspond to the number of points. :type weights: Iterable[float] - :param weight_file: A file containing a list of weights (one per line). - If a `weight_file` is given with `weights` as arguments, only weights from the - file are taken into account. - - :type weight_file: string - :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is 'safe'. :type precision: string - :raises FileNotFoundError: If `off_file` and/or `weight_file` is set but not found. + :raises FileNotFoundError: **[deprecated]** If `off_file` is set but not found. :raises ValueError: In case of inconsistency between the number of points and weights. """ # The real cython constructor - def __cinit__(self, points = [], off_file = '', weights=[], weight_file='', precision = 'safe'): + def __cinit__(self, points = [], off_file = '', weights=[], precision = 'safe'): assert precision in ['fast', 'safe', 'exact'], \ "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' if off_file: + warnings.warn("off_file is a deprecated parameter, please consider using gudhi.read_points_from_off_file", + DeprecationWarning) if os.path.isfile(off_file): points = read_points_from_off_file(off_file = off_file) else: raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), off_file) - if weight_file: - if os.path.isfile(weight_file): - weights = read_weights(weight_file = weight_file) - else: - raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), weight_file) - # weights are set but is inconsistent with the number of points if len(weights) != 0 and len(weights) != len(points): raise ValueError("Inconsistency between the number of points and weights") diff --git a/src/python/gudhi/reader_utils.pyx b/src/python/gudhi/reader_utils.pyx index f997ad3e..fe1c3a2e 100644 --- a/src/python/gudhi/reader_utils.pyx +++ b/src/python/gudhi/reader_utils.pyx @@ -84,19 +84,3 @@ def read_persistence_intervals_in_dimension(persistence_file='', only_this_dim=- 'utf-8'), only_this_dim)) print("file " + persistence_file + " not set or not found.") return [] - -def read_weights(weight_file=''): - """Reads a file containing weights. Only one float value per line is read and stored. - The return value is a `list(weight)`. - - :param weight_file: A weight file style name (one weight per line). - :type weight_file: string - - :returns: A list of weights. - :rtype: List[float] - """ - weights=[] - with open(weight_file, 'r') as wfile: - weights = [float(wline) for wline in wfile if wline.strip()] - return weights - diff --git a/src/python/test/test_alpha_complex.py b/src/python/test/test_alpha_complex.py index a0de46c3..e0f2b5df 100755 --- a/src/python/test/test_alpha_complex.py +++ b/src/python/test/test_alpha_complex.py @@ -12,6 +12,8 @@ import gudhi as gd import math import numpy as np import pytest +import warnings + try: # python3 from itertools import zip_longest @@ -203,7 +205,13 @@ def test_delaunay_complex(): _delaunay_complex(precision) def _3d_points_on_a_plane(precision, default_filtration_value): - alpha = gd.AlphaComplex(off_file='alphacomplexdoc.off', precision = precision) + alpha = gd.AlphaComplex(points = [[1.0, 1.0 , 0.0], + [7.0, 0.0 , 0.0], + [4.0, 6.0 , 0.0], + [9.0, 6.0 , 0.0], + [0.0, 14.0, 0.0], + [2.0, 19.0, 0.0], + [9.0, 17.0, 0.0]], precision = precision) simplex_tree = alpha.create_simplex_tree(default_filtration_value = default_filtration_value) assert simplex_tree.dimension() == 2 @@ -211,28 +219,16 @@ def _3d_points_on_a_plane(precision, default_filtration_value): assert simplex_tree.num_simplices() == 25 def test_3d_points_on_a_plane(): - off_file = open("alphacomplexdoc.off", "w") - off_file.write("OFF \n" \ - "7 0 0 \n" \ - "1.0 1.0 0.0\n" \ - "7.0 0.0 0.0\n" \ - "4.0 6.0 0.0\n" \ - "9.0 6.0 0.0\n" \ - "0.0 14.0 0.0\n" \ - "2.0 19.0 0.0\n" \ - "9.0 17.0 0.0\n" ) - off_file.close() - for default_filtration_value in [True, False]: for precision in ['fast', 'safe', 'exact']: _3d_points_on_a_plane(precision, default_filtration_value) def _3d_tetrahedrons(precision): points = 10*np.random.rand(10, 3) - alpha = gd.AlphaComplex(points=points, precision = precision) + alpha = gd.AlphaComplex(points = points, precision = precision) st_alpha = alpha.create_simplex_tree(default_filtration_value = False) # New AlphaComplex for get_point to work - delaunay = gd.AlphaComplex(points=points, precision = precision) + delaunay = gd.AlphaComplex(points = points, precision = precision) st_delaunay = delaunay.create_simplex_tree(default_filtration_value = True) delaunay_tetra = [] @@ -262,11 +258,7 @@ def test_3d_tetrahedrons(): for precision in ['fast', 'safe', 'exact']: _3d_tetrahedrons(precision) -def test_non_existing_off_file(): - with pytest.raises(FileNotFoundError): - alpha = gd.AlphaComplex(off_file="pouetpouettralala.toubiloubabdou") - -def test_non_existing_weight_file(): +def test_off_file_deprecation_warning(): off_file = open("alphacomplexdoc.off", "w") off_file.write("OFF \n" \ "7 0 0 \n" \ @@ -279,67 +271,32 @@ def test_non_existing_weight_file(): "9.0 17.0 0.0\n" ) off_file.close() - with pytest.raises(FileNotFoundError): - alpha = gd.AlphaComplex(off_file="alphacomplexdoc.off", - weight_file="pouetpouettralala.toubiloubabdou") + with pytest.warns(DeprecationWarning): + alpha = gd.AlphaComplex(off_file="alphacomplexdoc.off") +def test_non_existing_off_file(): + with pytest.raises(FileNotFoundError): + alpha = gd.AlphaComplex(off_file="pouetpouettralala.toubiloubabdou") -def test_inconsistency_off_weight_file(): - off_file = open("alphacomplexdoc.off", "w") - off_file.write("OFF \n" \ - "7 0 0 \n" \ - "1.0 1.0 0.0\n" \ - "7.0 0.0 0.0\n" \ - "4.0 6.0 0.0\n" \ - "9.0 6.0 0.0\n" \ - "0.0 14.0 0.0\n" \ - "2.0 19.0 0.0\n" \ - "9.0 17.0 0.0\n" ) - off_file.close() - # 7 points, 8 weights, on purpose - weight_file = open("alphacomplexdoc.wgt", "w") - weight_file.write("5.0\n" \ - "2.0\n" \ - "7.0\n" \ - "4.0\n" \ - "9.0\n" \ - "0.0\n" \ - "2.0\n" \ - "9.0\n" ) - weight_file.close() - +def test_inconsistency_points_and_weights(): + points = [[1.0, 1.0 , 0.0], + [7.0, 0.0 , 0.0], + [4.0, 6.0 , 0.0], + [9.0, 6.0 , 0.0], + [0.0, 14.0, 0.0], + [2.0, 19.0, 0.0], + [9.0, 17.0, 0.0]] with pytest.raises(ValueError): - alpha = gd.AlphaComplex(off_file="alphacomplexdoc.off", - weight_file="alphacomplexdoc.wgt") + # 7 points, 8 weights, on purpose + alpha = gd.AlphaComplex(points = points, + weights = [1., 2., 3., 4., 5., 6., 7., 8.]) - # 7 points, 6 weights, on purpose with pytest.raises(ValueError): - alpha = gd.AlphaComplex(off_file="alphacomplexdoc.off", - weights=[1., 2., 3., 4., 5., 6.]) - -def _with_or_without_weight_file(precision): - off_file = open("weightalphacomplex.off", "w") - off_file.write("OFF \n" \ - "5 0 0 \n" \ - "1. -1. -1. \n" \ - "-1. 1. -1. \n" \ - "-1. -1. 1. \n" \ - "1. 1. 1. \n" \ - "2. 2. 2.") - off_file.close() - - weight_file = open("weightalphacomplex.wgt", "w") - weight_file.write("4.0\n" \ - "4.0\n" \ - "4.0\n" \ - "4.0\n" \ - "1.0\n" ) - weight_file.close() - - stree_from_files = gd.AlphaComplex(off_file="weightalphacomplex.off", - weight_file="weightalphacomplex.wgt", - precision = precision).create_simplex_tree() + # 7 points, 6 weights, on purpose + alpha = gd.AlphaComplex(points = points, + weights = [1., 2., 3., 4., 5., 6.]) +def _doc_example(precision): stree_from_values = gd.AlphaComplex(points=[[ 1., -1., -1.], [-1., 1., -1.], [-1., -1., 1.], @@ -348,8 +305,11 @@ def _with_or_without_weight_file(precision): weights = [4., 4., 4., 4., 1.], precision = precision).create_simplex_tree() - assert stree_from_files == stree_from_values + assert stree_from_values.filtration([0, 1, 2, 3]) == pytest.approx(-1.) + assert stree_from_values.filtration([0, 1, 3, 4]) == pytest.approx(95.) + assert stree_from_values.filtration([0, 2, 3, 4]) == pytest.approx(95.) + assert stree_from_values.filtration([1, 2, 3, 4]) == pytest.approx(95.) -def test_with_or_without_weight_file(): +def test_doc_example(): for precision in ['fast', 'safe', 'exact']: - _with_or_without_weight_file(precision) + _doc_example(precision) diff --git a/src/python/test/test_reader_utils.py b/src/python/test/test_reader_utils.py index 91de9ba0..4fc7c00f 100755 --- a/src/python/test/test_reader_utils.py +++ b/src/python/test/test_reader_utils.py @@ -125,19 +125,3 @@ def test_read_persistence_intervals_with_dimension(): 1: [(9.6, 14.0), (3.0, float("Inf"))], 3: [(34.2, 34.974)], } - - -def test_non_existing_weights_file(): - with raises(FileNotFoundError): - # Try to open a non existing file - persistence = gd.read_weights(weight_file="pouetpouettralala.toubiloubabdou") - -def test_read_weights(): - # Create test file - test_file = open("test_read_weights.wgt", "w") - test_file.write( - "2.7\n 9.6 \n\t34.2\n3.\t\n\n" - ) - test_file.close() - weights = gd.read_weights(weight_file = "test_read_weights.wgt") - assert weights == [2.7, 9.6, 34.2, 3.] -- cgit v1.2.3 From 953a7da68657b9ab493cfb35b41fc46e26a73876 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 25 Mar 2021 08:52:50 +0100 Subject: Add equality operator for python Simplex tree --- src/python/gudhi/simplex_tree.pxd | 1 + src/python/gudhi/simplex_tree.pyx | 7 +++++++ src/python/test/test_simplex_tree.py | 12 ++++++++++++ 3 files changed, 20 insertions(+) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index 000323af..3df614dd 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -65,6 +65,7 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": vector[vector[pair[int, pair[double, double]]]] compute_extended_persistence_subdiagrams(vector[pair[int, pair[double, double]]] dgm, double min_persistence) nogil Simplex_tree_interface_full_featured* collapse_edges(int nb_collapse_iteration) nogil except + void reset_filtration(double filtration, int dimension) nogil + bint operator==(Simplex_tree_interface_full_featured) nogil # Iterators over Simplex tree pair[vector[int], double] get_simplex_and_filtration(Simplex_tree_simplex_handle f_simplex) nogil Simplex_tree_simplices_iterator get_simplices_iterator_begin() nogil diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index d7991417..0bfaa19f 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -639,3 +639,10 @@ cdef class SimplexTree: self.thisptr = (ptr.collapse_edges(nb_iter)) # Delete old pointer del ptr + + def __eq__(self, other:SimplexTree): + """Simplex tree equality operator using C++ depth first search operator== + :returns: True if the 2 simplex trees are equal, False otherwise. + :rtype: bool + """ + return dereference(self.get_ptr()) == dereference(other.get_ptr()) \ No newline at end of file diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index a3eacaa9..92b909ca 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -404,3 +404,15 @@ def test_boundaries_iterator(): with pytest.raises(RuntimeError): list(st.get_boundaries([6])) # (6) does not exist + +def test_equality_operator(): + st1 = SimplexTree() + st2 = SimplexTree() + + assert st1 == st2 + + st1.insert([1,2,3], 4.) + assert st1 != st2 + + st2.insert([1,2,3], 4.) + assert st1 == st2 \ No newline at end of file -- cgit v1.2.3 From 98cc06acb5f4b7caf4c23645614a472f7f9b5f3a Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 25 Mar 2021 09:04:27 +0100 Subject: Move simplex tree equality operator in another branch --- src/python/gudhi/simplex_tree.pxd | 1 - src/python/gudhi/simplex_tree.pyx | 9 --------- src/python/test/test_simplex_tree.py | 12 ------------ 3 files changed, 22 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index 2aa435b1..000323af 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -74,7 +74,6 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": Simplex_tree_skeleton_iterator get_skeleton_iterator_begin(int dimension) nogil Simplex_tree_skeleton_iterator get_skeleton_iterator_end(int dimension) nogil pair[Simplex_tree_boundary_iterator, Simplex_tree_boundary_iterator] get_boundary_iterators(vector[int] simplex) nogil except + - bint operator==(Simplex_tree_interface_full_featured) nogil cdef extern from "Persistent_cohomology_interface.h" namespace "Gudhi": cdef cppclass Simplex_tree_persistence_interface "Gudhi::Persistent_cohomology_interface>": diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index b5a938d5..d7991417 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -639,12 +639,3 @@ cdef class SimplexTree: self.thisptr = (ptr.collapse_edges(nb_iter)) # Delete old pointer del ptr - - def __eq__(self, other): - """Simplex tree equality operator using C++ depth first search operator== - - :returns: True if the 2 simplex trees are equal, False otherwise. - :rtype: bool - """ - cdef intptr_t other_int_ptr=other.thisptr - return dereference(self.get_ptr()) == dereference(other_int_ptr) \ No newline at end of file diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index 83b5c268..a3eacaa9 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -404,15 +404,3 @@ def test_boundaries_iterator(): with pytest.raises(RuntimeError): list(st.get_boundaries([6])) # (6) does not exist - -def test_equality_operator(): - st1 = SimplexTree() - st2 = SimplexTree() - - assert st1 == st2 - - st1.insert([1,2,3], 4.) - assert st1 != st2 - - st2.insert([1,2,3], 4.) - assert st1 == st2 -- cgit v1.2.3 From e4381a3e2ad79d3150cd03704bef3fc006e7c54b Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Sat, 3 Apr 2021 10:27:08 +0200 Subject: Python alpha complex specific 3d with weighted version and functor to get points --- src/python/CMakeLists.txt | 2 + src/python/gudhi/alpha_complex_3d.pyx | 129 ++++++++++++++++++++++++ src/python/include/Alpha_complex_factory.h | 48 +++++++-- src/python/include/Alpha_complex_interface.h | 59 ++++------- src/python/include/Alpha_complex_interface_3d.h | 71 +++++++++++++ 5 files changed, 261 insertions(+), 48 deletions(-) create mode 100644 src/python/gudhi/alpha_complex_3d.pyx create mode 100644 src/python/include/Alpha_complex_interface_3d.h (limited to 'src/python/gudhi') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 73303a24..307181b7 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -61,6 +61,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'subsampling', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'tangential_complex', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'alpha_complex', ") + set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'alpha_complex_3d', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'euclidean_witness_complex', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'euclidean_strong_witness_complex', ") # Modules that should not be auto-imported in __init__.py @@ -156,6 +157,7 @@ if(PYTHONINTERP_FOUND) endif () if (NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'alpha_complex', ") + set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'alpha_complex_3d', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'subsampling', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'tangential_complex', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'euclidean_witness_complex', ") diff --git a/src/python/gudhi/alpha_complex_3d.pyx b/src/python/gudhi/alpha_complex_3d.pyx new file mode 100644 index 00000000..3959004a --- /dev/null +++ b/src/python/gudhi/alpha_complex_3d.pyx @@ -0,0 +1,129 @@ +# 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): Vincent Rouvreau +# +# Copyright (C) 2021 Inria +# +# Modification(s): +# - YYYY/MM Author: Description of the modification + +from __future__ import print_function +from cython cimport numeric +from libcpp.vector cimport vector +from libcpp.utility cimport pair +from libcpp.string cimport string +from libcpp cimport bool +from libc.stdint cimport intptr_t +import errno +import os +import warnings + +from gudhi.simplex_tree cimport * +from gudhi.simplex_tree import SimplexTree +from gudhi import read_points_from_off_file + +__author__ = "Vincent Rouvreau" +__copyright__ = "Copyright (C) 2021 Inria" +__license__ = "GPL v3" + +cdef extern from "Alpha_complex_interface_3d.h" namespace "Gudhi": + cdef cppclass Alpha_complex_interface_3d "Gudhi::alpha_complex::Alpha_complex_interface_3d": + Alpha_complex_interface_3d(vector[vector[double]] points, vector[double] weights, bool fast_version, bool exact_version) nogil except + + vector[double] get_point(int vertex) nogil except + + void create_simplex_tree(Simplex_tree_interface_full_featured* simplex_tree, double max_alpha_square) nogil except + + +# AlphaComplex3D python interface +cdef class AlphaComplex3D: + """AlphaComplex3D is a simplicial complex constructed from the finite cells + of a Delaunay Triangulation. + + The filtration value of each simplex is computed as the square of the + circumradius of the simplex if the circumsphere is empty (the simplex is + then said to be Gabriel), and as the minimum of the filtration values of + the codimension 1 cofaces that make it not Gabriel otherwise. + + All simplices that have a filtration value strictly greater than a given + alpha squared value are not inserted into the complex. + + .. note:: + + When AlphaComplex3D is constructed with an infinite value of alpha, the + complex is a Delaunay complex. + + """ + + cdef Alpha_complex_interface_3d * this_ptr + + # Fake constructor that does nothing but documenting the constructor + def __init__(self, points=[], weights=[], precision='safe'): + """AlphaComplex3D constructor. + + :param points: A list of points in d-Dimension. + :type points: Iterable[Iterable[float]] + + :param weights: A list of weights. If set, the number of weights must correspond to the + number of points. + :type weights: Iterable[float] + + :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is + 'safe'. + :type precision: string + + :raises ValueError: In case of inconsistency between the number of points and weights. + """ + + # The real cython constructor + def __cinit__(self, points = [], weights=[], precision = 'safe'): + assert precision in ['fast', 'safe', 'exact'], \ + "Alpha complex precision can only be 'fast', 'safe' or 'exact'" + cdef bool fast = precision == 'fast' + cdef bool exact = precision == 'exact' + + # weights are set but is inconsistent with the number of points + if len(weights) != 0 and len(weights) != len(points): + raise ValueError("Inconsistency between the number of points and weights") + + # need to copy the points to use them without the gil + cdef vector[vector[double]] pts + cdef vector[double] wgts + pts = points + wgts = weights + with nogil: + self.this_ptr = new Alpha_complex_interface_3d(pts, wgts, fast, exact) + + def __dealloc__(self): + if self.this_ptr != NULL: + del self.this_ptr + + def __is_defined(self): + """Returns true if AlphaComplex3D pointer is not NULL. + """ + return self.this_ptr != NULL + + def get_point(self, vertex): + """This function returns the point corresponding to a given vertex from the :class:`~gudhi.SimplexTree`. + + :param vertex: The vertex. + :type vertex: int + :rtype: list of float + :returns: the point. + """ + return self.this_ptr.get_point(vertex) + + def create_simplex_tree(self, max_alpha_square = float('inf')): + """ + :param max_alpha_square: The maximum alpha square threshold the simplices shall not exceed. Default is set to + infinity, and there is very little point using anything else since it does not save time. + :type max_alpha_square: float + :returns: A simplex tree created from the Delaunay Triangulation. + :rtype: SimplexTree + """ + stree = SimplexTree() + cdef double mas = max_alpha_square + cdef intptr_t stree_int_ptr=stree.thisptr + with nogil: + self.this_ptr.create_simplex_tree(stree_int_ptr, + mas) + return stree diff --git a/src/python/include/Alpha_complex_factory.h b/src/python/include/Alpha_complex_factory.h index 36e98615..5d3bfb65 100644 --- a/src/python/include/Alpha_complex_factory.h +++ b/src/python/include/Alpha_complex_factory.h @@ -31,6 +31,34 @@ namespace Gudhi { namespace alpha_complex { +template +struct Point_cgal_to_cython; + +template +struct Point_cgal_to_cython { + std::vector operator()(CgalPointType const& point) const + { + std::vector vd; + vd.reserve(point.dimension()); + for (auto coord = point.cartesian_begin(); coord != point.cartesian_end(); coord++) + vd.push_back(CGAL::to_double(*coord)); + return vd; + } +}; + +template +struct Point_cgal_to_cython { + std::vector operator()(CgalPointType const& weighted_point) const + { + auto point = weighted_point.point(); + std::vector vd; + vd.reserve(point.dimension()); + for (auto coord = point.cartesian_begin(); coord != point.cartesian_end(); coord++) + vd.push_back(CGAL::to_double(*coord)); + return vd; + } +}; + template std::vector pt_cgal_to_cython(CgalPointType const& point) { std::vector vd; @@ -159,13 +187,14 @@ class Inexact_weighted_alpha_complex_dD final : public Abstract_alpha_complex { Alpha_complex alpha_complex_; }; -template +template class Alpha_complex_3D final : public Abstract_alpha_complex { private: - using Point = typename Alpha_complex_3d::Bare_point_3; + using Bare_point = typename Alpha_complex_3d::Bare_point_3; + using Point = typename Alpha_complex_3d::Point_3; - static Point pt_cython_to_cgal_3(std::vector const& vec) { - return Point(vec[0], vec[1], vec[2]); + static Bare_point pt_cython_to_cgal_3(std::vector const& vec) { + return Bare_point(vec[0], vec[1], vec[2]); } public: @@ -173,18 +202,23 @@ class Alpha_complex_3D final : public Abstract_alpha_complex { : alpha_complex_(boost::adaptors::transform(points, pt_cython_to_cgal_3)) { } + Alpha_complex_3D(const std::vector>& points, const std::vector& weights) + : alpha_complex_(boost::adaptors::transform(points, pt_cython_to_cgal_3), weights) { + } + virtual std::vector get_point(int vh) override { Point const& point = alpha_complex_.get_point(vh); - return pt_cgal_to_cython(point); + return Point_cgal_to_cython()(point); } virtual bool create_simplex_tree(Simplex_tree_interface<>* simplex_tree, double max_alpha_square, bool default_filtration_value) override { - return alpha_complex_.create_complex(*simplex_tree, max_alpha_square); + alpha_complex_.create_complex(*simplex_tree, max_alpha_square); + return true; } private: - Alpha_complex_3d alpha_complex_; + Alpha_complex_3d alpha_complex_; }; diff --git a/src/python/include/Alpha_complex_interface.h b/src/python/include/Alpha_complex_interface.h index 43c96b2f..31a8147b 100644 --- a/src/python/include/Alpha_complex_interface.h +++ b/src/python/include/Alpha_complex_interface.h @@ -30,10 +30,20 @@ class Alpha_complex_interface { Alpha_complex_interface(const std::vector>& points, const std::vector& weights, bool fast_version, bool exact_version) - : points_(points), - weights_(weights), - fast_version_(fast_version), - exact_version_(exact_version) { + : empty_point_set_(points.size() == 0) { + if (fast_version) { + if (weights.size() == 0) { + alpha_ptr_ = std::make_unique(points, exact_version); + } else { + alpha_ptr_ = std::make_unique(points, weights, exact_version); + } + } else { + if (weights.size() == 0) { + alpha_ptr_ = std::make_unique(points, exact_version); + } else { + alpha_ptr_ = std::make_unique(points, weights, exact_version); + } + } } std::vector get_point(int vh) { @@ -42,47 +52,14 @@ class Alpha_complex_interface { void create_simplex_tree(Simplex_tree_interface<>* simplex_tree, double max_alpha_square, bool default_filtration_value) { - if (points_.size() > 0) { - std::size_t dimension = points_[0].size(); - if (dimension == 3 && weights_.size() == 0 && !default_filtration_value) { - if (fast_version_) - alpha_ptr_ = std::make_unique>(points_); - else if (exact_version_) - alpha_ptr_ = std::make_unique>(points_); - else - alpha_ptr_ = std::make_unique>(points_); - if (!alpha_ptr_->create_simplex_tree(simplex_tree, max_alpha_square, default_filtration_value)) { - // create_simplex_tree will fail if all points are on a plane - Retry with dD by setting dimension to 2 - dimension--; - alpha_ptr_.reset(); - } - } - // Not ** else ** because we have to take into account if 3d fails - if (dimension != 3 || weights_.size() != 0 || default_filtration_value) { - if (fast_version_) { - if (weights_.size() == 0) { - alpha_ptr_ = std::make_unique(points_, exact_version_); - } else { - alpha_ptr_ = std::make_unique(points_, weights_, exact_version_); - } - } else { - if (weights_.size() == 0) { - alpha_ptr_ = std::make_unique(points_, exact_version_); - } else { - alpha_ptr_ = std::make_unique(points_, weights_, exact_version_); - } - } - alpha_ptr_->create_simplex_tree(simplex_tree, max_alpha_square, default_filtration_value); - } - } + // Nothing to be done in case of an empty point set + if (!empty_point_set_) + alpha_ptr_->create_simplex_tree(simplex_tree, max_alpha_square, default_filtration_value); } private: std::unique_ptr alpha_ptr_; - std::vector> points_; - std::vector weights_; - bool fast_version_; - bool exact_version_; + bool empty_point_set_; }; } // namespace alpha_complex diff --git a/src/python/include/Alpha_complex_interface_3d.h b/src/python/include/Alpha_complex_interface_3d.h new file mode 100644 index 00000000..bb66b8e1 --- /dev/null +++ b/src/python/include/Alpha_complex_interface_3d.h @@ -0,0 +1,71 @@ +/* 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): Vincent Rouvreau + * + * Copyright (C) 2021 Inria + * + * Modification(s): + * - YYYY/MM Author: Description of the modification + */ + +#ifndef INCLUDE_ALPHA_COMPLEX_INTERFACE_3D_H_ +#define INCLUDE_ALPHA_COMPLEX_INTERFACE_3D_H_ + +#include "Alpha_complex_factory.h" +#include + +#include "Simplex_tree_interface.h" + +#include +#include +#include +#include // for std::unique_ptr + +namespace Gudhi { + +namespace alpha_complex { + +class Alpha_complex_interface_3d { + public: + Alpha_complex_interface_3d(const std::vector>& points, + const std::vector& weights, + bool fast_version, bool exact_version) + : empty_point_set_(points.size() == 0) { + const bool weighted = (weights.size() > 0); + if (fast_version) + if (weighted) + alpha_ptr_ = std::make_unique>(points, weights); + else + alpha_ptr_ = std::make_unique>(points); + else if (exact_version) + if (weighted) + alpha_ptr_ = std::make_unique>(points, weights); + else + alpha_ptr_ = std::make_unique>(points); + else + if (weighted) + alpha_ptr_ = std::make_unique>(points, weights); + else + alpha_ptr_ = std::make_unique>(points); + } + + std::vector get_point(int vh) { + return alpha_ptr_->get_point(vh); + } + + void create_simplex_tree(Simplex_tree_interface<>* simplex_tree, double max_alpha_square) { + // Nothing to be done in case of an empty point set + if (!empty_point_set_) + alpha_ptr_->create_simplex_tree(simplex_tree, max_alpha_square, false); + } + + private: + std::unique_ptr alpha_ptr_; + bool empty_point_set_; +}; + +} // namespace alpha_complex + +} // namespace Gudhi + +#endif // INCLUDE_ALPHA_COMPLEX_INTERFACE_3D_H_ -- cgit v1.2.3 From a6e7f96f7d2c391e4548309174cc05f5ae05d871 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau <10407034+VincentRouvreau@users.noreply.github.com> Date: Thu, 15 Apr 2021 22:48:10 +0200 Subject: Update src/python/gudhi/simplex_tree.pyx Co-authored-by: Marc Glisse --- src/python/gudhi/simplex_tree.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 0bfaa19f..67428401 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -641,8 +641,8 @@ cdef class SimplexTree: del ptr def __eq__(self, other:SimplexTree): - """Simplex tree equality operator using C++ depth first search operator== + """Test for structural equality :returns: True if the 2 simplex trees are equal, False otherwise. :rtype: bool """ - return dereference(self.get_ptr()) == dereference(other.get_ptr()) \ No newline at end of file + return dereference(self.get_ptr()) == dereference(other.get_ptr()) -- cgit v1.2.3 From f4f5992e9025d055e35c86589923f1b0299f552e Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 22 Apr 2021 08:39:03 +0200 Subject: code review: rename block_func when it is a type. Type is a 'callable' --- src/python/gudhi/simplex_tree.pxd | 4 ++-- src/python/gudhi/simplex_tree.pyx | 2 +- src/python/include/Simplex_tree_interface.h | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index 80c6ffca..ff750af9 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -67,8 +67,8 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": Simplex_tree_skeleton_iterator get_skeleton_iterator_begin(int dimension) nogil Simplex_tree_skeleton_iterator get_skeleton_iterator_end(int dimension) nogil # Expansion with blockers - ctypedef bool (*blocker_func)(vector[int], void *user_data) - void expansion_with_blockers_callback(int dimension, blocker_func user_func, void *user_data) + ctypedef bool (*blocker_func_t)(vector[int], void *user_data) + void expansion_with_blockers_callback(int dimension, blocker_func_t user_func, void *user_data) cdef extern from "Persistent_cohomology_interface.h" namespace "Gudhi": cdef cppclass Simplex_tree_persistence_interface "Gudhi::Persistent_cohomology_interface>": diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index debe92c0..383d1949 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -428,7 +428,7 @@ cdef class SimplexTree: :param max_dim: Expansion maximal dimension value. :type max_dim: int :param blocker_func: Blocker oracle. - :type blocker_func: Its concept is `Boolean blocker_func(list of int)` + :type blocker_func: Callable[[List[int]], bool] """ self.get_ptr().expansion_with_blockers_callback(max_dim, callback, blocker_func) diff --git a/src/python/include/Simplex_tree_interface.h b/src/python/include/Simplex_tree_interface.h index 9f92b349..a859bec0 100644 --- a/src/python/include/Simplex_tree_interface.h +++ b/src/python/include/Simplex_tree_interface.h @@ -39,7 +39,7 @@ class Simplex_tree_interface : public Simplex_tree { using Skeleton_simplex_iterator = typename Base::Skeleton_simplex_iterator; using Complex_simplex_iterator = typename Base::Complex_simplex_iterator; using Extended_filtration_data = typename Base::Extended_filtration_data; - typedef bool (*blocker_func)(Simplex simplex, void *user_data); + typedef bool (*blocker_func_t)(Simplex simplex, void *user_data); public: @@ -189,7 +189,7 @@ class Simplex_tree_interface : public Simplex_tree { return collapsed_stree_ptr; } - void expansion_with_blockers_callback(int dimension, blocker_func user_func, void *user_data) { + void expansion_with_blockers_callback(int dimension, blocker_func_t user_func, void *user_data) { Base::expansion_with_blockers(dimension, [&](Simplex_handle sh){ Simplex simplex; for (auto vertex : Base::simplex_vertex_range(sh)) { -- cgit v1.2.3 From 7d3fba5d1561b3241b914583ac420434e788e27f Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Wed, 28 Apr 2021 16:11:34 +0200 Subject: Handle an empty list of persistence diagrams --- src/python/gudhi/representations/vector_methods.py | 6 ++++++ src/python/test/test_betti_curve_representations.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 5133a64c..82f071d7 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -417,6 +417,9 @@ class BettiCurve2(BaseEstimator, TransformerMixin): """ if self.predefined_grid is None: + if not X: + X = [np.zeros((0, 2))] + N = len(X) events = np.concatenate([pd.flatten(order="F") for pd in X], axis=0) @@ -469,6 +472,9 @@ class BettiCurve2(BaseEstimator, TransformerMixin): if not self.is_fitted(): raise NotFittedError("Not fitted.") + if not X: + X = [np.zeros((0, 2))] + N = len(X) events = np.concatenate([pd.flatten(order="F") for pd in X], axis=0) diff --git a/src/python/test/test_betti_curve_representations.py b/src/python/test/test_betti_curve_representations.py index 5b95fa2c..475839ee 100755 --- a/src/python/test/test_betti_curve_representations.py +++ b/src/python/test/test_betti_curve_representations.py @@ -37,3 +37,18 @@ def test_betti_curve_is_irregular_betti_curve_followed_by_interpolation(): interp = scipy.interpolate.interp1d(bc.grid_, bettis[i, :], kind="previous", fill_value="extrapolate") bettis_interp = np.array(interp(grid), dtype=int) assert((bettis_interp == bettis_gridded).all()) + + +def test_empty_with_predefined_grid(): + random_grid = np.sort(np.random.uniform(0, 1, 100)) + bc = BettiCurve2(random_grid) + bettis = bc.fit_transform([]) + assert((bc.grid_ == random_grid).all()) + assert((bettis == 0).all()) + + +def test_empty(): + bc = BettiCurve2() + bettis = bc.fit_transform([]) + assert(bc.grid_ == [-np.inf]) + assert((bettis == 0).all()) -- 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/gudhi') 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 9841a3c845905c9b278ddb7828260a3d6fa5fce7 Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Fri, 30 Apr 2021 15:08:19 +0200 Subject: Allow specifying range for uniform predefined grid for compatibility with old class --- src/python/gudhi/representations/vector_methods.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 82f071d7..86afaa1c 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -359,8 +359,8 @@ class BettiCurve2(BaseEstimator, TransformerMixin): Parameters ---------- - predefined_grid: 1d array or None, default=None - Predefined filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinities are OK. If None (default), a grid will be computed that captures all changes in Betti numbers in the provided data. + predefined_grid: 1d array, triple or None, default=None + Predefined filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinities are OK. If a triple of the form (l, u, n), the grid will be uniform from l to u in n steps. If None (default), a grid will be computed that captures all changes in Betti numbers in the provided data. Attributes ---------- @@ -382,7 +382,13 @@ class BettiCurve2(BaseEstimator, TransformerMixin): """ def __init__(self, predefined_grid = None): - self.predefined_grid = predefined_grid + if isinstance(predefined_grid, tuple): + if len(predefined_grid) != 3: + raise ValueError("Expected array, None or triple.") + + self.predefined_grid = np.linspace(predefined_grid[0], predefined_grid[1], predefined_grid[2]) + else: + self.predefined_grid = predefined_grid def is_fitted(self): -- cgit v1.2.3 From 09fe9bd25d9212fa42b77570a0ef80bc97d742be Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Fri, 30 Apr 2021 15:08:56 +0200 Subject: Replace old BettiCurve class --- src/python/gudhi/representations/vector_methods.py | 67 +--------------------- 1 file changed, 1 insertion(+), 66 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 86afaa1c..bdbaa175 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -287,73 +287,8 @@ class Silhouette(BaseEstimator, TransformerMixin): """ return self.fit_transform([diag])[0,:] -class BettiCurve(BaseEstimator, TransformerMixin): - """ - This is a class for computing Betti curves from a list of persistence diagrams. A Betti curve is a 1D piecewise-constant function obtained from the rank function. It is sampled evenly on a given range and the vector of samples is returned. See https://www.researchgate.net/publication/316604237_Time_Series_Classification_via_Topological_Data_Analysis for more details. - """ - def __init__(self, resolution=100, sample_range=[np.nan, np.nan]): - """ - Constructor for the BettiCurve class. - - Parameters: - resolution (int): number of sample for the piecewise-constant function (default 100). - sample_range ([double, double]): minimum and maximum of the piecewise-constant function domain, of the form [x_min, x_max] (default [numpy.nan, numpy.nan]). It is the interval on which samples will be drawn evenly. If one of the values is numpy.nan, it can be computed from the persistence diagrams with the fit() method. - """ - self.resolution, self.sample_range = resolution, sample_range - - def fit(self, X, y=None): - """ - Fit the BettiCurve class on a list of persistence diagrams: if any of the values in **sample_range** is numpy.nan, replace it with the corresponding value computed on the given list of persistence diagrams. - - Parameters: - 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)) - return self - - def transform(self, X): - """ - Compute the Betti curve for each persistence diagram individually and concatenate the results. - - Parameters: - X (list of n x 2 numpy arrays): input persistence diagrams. - - Returns: - numpy array with shape (number of diagrams) x (**resolution**): output Betti curves. - """ - Xfit = [] - x_values = np.linspace(self.sample_range[0], self.sample_range[1], self.resolution) - step_x = x_values[1] - x_values[0] - - for diagram in X: - diagram_int = np.clip(np.ceil((diagram[:,:2] - self.sample_range[0]) / step_x), 0, self.resolution).astype(int) - bc = np.zeros(self.resolution) - for interval in diagram_int: - bc[interval[0]:interval[1]] += 1 - Xfit.append(np.reshape(bc,[1,-1])) - - Xfit = np.concatenate(Xfit, 0) - - return Xfit - def __call__(self, diag): - """ - Apply BettiCurve on a single persistence diagram and outputs the result. - - Parameters: - diag (n x 2 numpy array): input persistence diagram. - - Returns: - numpy array with shape (**resolution**): output Betti curve. - """ - return self.fit_transform([diag])[0,:] - - -class BettiCurve2(BaseEstimator, TransformerMixin): +class BettiCurve(BaseEstimator, TransformerMixin): """ A more flexible replacement for the BettiCurve class. There are two modes of operation: with a predefined grid, and without. With a predefined grid, the class computes the Betti numbers at those grid points. Without a predefined grid, it can be fit to a list of persistence diagrams and produce a grid that consists of (at least) the filtration values at which at least one of those persistence diagrams chance Betti numbers, and then compute the Betti numbers at those grid points. In the latter mode, the exact Betti curve is computed for the entire real line. -- 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/gudhi') 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/gudhi') 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/gudhi') 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 241cc1422e9362c23db1c4c25ba8b63f88a1153f Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 16 May 2021 14:21:05 +0200 Subject: Update doc string to reflect new class name --- src/python/gudhi/representations/vector_methods.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index bdbaa175..7e615b70 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -290,7 +290,7 @@ class Silhouette(BaseEstimator, TransformerMixin): class BettiCurve(BaseEstimator, TransformerMixin): """ - A more flexible replacement for the BettiCurve class. There are two modes of operation: with a predefined grid, and without. With a predefined grid, the class computes the Betti numbers at those grid points. Without a predefined grid, it can be fit to a list of persistence diagrams and produce a grid that consists of (at least) the filtration values at which at least one of those persistence diagrams chance Betti numbers, and then compute the Betti numbers at those grid points. In the latter mode, the exact Betti curve is computed for the entire real line. + Compute Betti curves from persistence diagrams. There are two modes of operation: with a predefined grid, and without. With a predefined grid, the class computes the Betti numbers at those grid points. Without a predefined grid, it can be fit to a list of persistence diagrams and produce a grid that consists of (at least) the filtration values at which at least one of those persistence diagrams chance Betti numbers, and then compute the Betti numbers at those grid points. In the latter mode, the exact Betti curve is computed for the entire real line. Parameters ---------- @@ -305,11 +305,11 @@ class BettiCurve(BaseEstimator, TransformerMixin): Examples -------- If pd is a persistence diagram and xs is a nonempty grid of finite values such that xs[0] >= pd.min(), then the result of - >>> bc = BettiCurve2(xs) + >>> bc = BettiCurve(xs) >>> result = bc(pd) and >>> from scipy.interpolate import interp1d - >>> bc = BettiCurve2(None) + >>> bc = BettiCurve(None) >>> bettis = bc.fit_transform([pd]) >>> interp = interp1d(bc.grid_, bettis[0, :], kind="previous", fill_value="extrapolate") >>> result = np.array(interp(xs), dtype=int) -- cgit v1.2.3 From ec55f3e92e96951508f4b8b5b3e1704d33d1d015 Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 16 May 2021 14:21:32 +0200 Subject: Typo --- src/python/gudhi/representations/vector_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 7e615b70..814b6081 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -290,7 +290,7 @@ class Silhouette(BaseEstimator, TransformerMixin): class BettiCurve(BaseEstimator, TransformerMixin): """ - Compute Betti curves from persistence diagrams. There are two modes of operation: with a predefined grid, and without. With a predefined grid, the class computes the Betti numbers at those grid points. Without a predefined grid, it can be fit to a list of persistence diagrams and produce a grid that consists of (at least) the filtration values at which at least one of those persistence diagrams chance Betti numbers, and then compute the Betti numbers at those grid points. In the latter mode, the exact Betti curve is computed for the entire real line. + Compute Betti curves from persistence diagrams. There are two modes of operation: with a predefined grid, and without. With a predefined grid, the class computes the Betti numbers at those grid points. Without a predefined grid, it can be fit to a list of persistence diagrams and produce a grid that consists of (at least) the filtration values at which at least one of those persistence diagrams changes Betti numbers, and then compute the Betti numbers at those grid points. In the latter mode, the exact Betti curve is computed for the entire real line. Parameters ---------- -- 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/gudhi') 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/gudhi') 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 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/gudhi') 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 afd0a890b39e40cd3ce358c647ae5e77eb288e07 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Wed, 9 Jun 2021 10:43:02 +0200 Subject: code review: weights should be None by default and not an empty list, clearer --- src/python/gudhi/alpha_complex.pyx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index 5d181391..caebfab0 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -82,7 +82,7 @@ cdef class AlphaComplex: """ # The real cython constructor - def __cinit__(self, points = [], off_file = '', weights=[], precision = 'safe'): + def __cinit__(self, points = [], off_file = '', weights=None, precision = 'safe'): assert precision in ['fast', 'safe', 'exact'], \ "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' @@ -97,14 +97,15 @@ cdef class AlphaComplex: raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), off_file) # weights are set but is inconsistent with the number of points - if len(weights) != 0 and len(weights) != len(points): + if weights != None and len(weights) != len(points): raise ValueError("Inconsistency between the number of points and weights") # need to copy the points to use them without the gil cdef vector[vector[double]] pts cdef vector[double] wgts pts = points - wgts = weights + if weights != None: + wgts = weights with nogil: self.this_ptr = new Alpha_complex_interface(pts, wgts, fast, exact) -- cgit v1.2.3 From f1effb5b5bf30275a692b51528ec068f7cef84ab Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Wed, 9 Jun 2021 11:47:34 +0200 Subject: doc review: use 120 characters per line and warning about the numbering of vertices for 3d version --- src/python/doc/alpha_complex_user.rst | 10 +++++++--- src/python/gudhi/alpha_complex.pyx | 33 ++++++++++++--------------------- src/python/gudhi/alpha_complex_3d.pyx | 31 +++++++++++++++---------------- 3 files changed, 34 insertions(+), 40 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/doc/alpha_complex_user.rst b/src/python/doc/alpha_complex_user.rst index 985ee962..e615d0be 100644 --- a/src/python/doc/alpha_complex_user.rst +++ b/src/python/doc/alpha_complex_user.rst @@ -34,8 +34,6 @@ Remarks the computation of filtration values can exceptionally be arbitrarily bad. In all cases, we still guarantee that the output is a valid filtration (faces have a filtration value no larger than their cofaces). * For performances reasons, it is advised to use Alpha_complex with `CGAL `_ :math:`\geq` 5.0.0. -* The vertices in the output simplex tree are not guaranteed to match the order of the input points. One can use - :func:`~gudhi.AlphaComplex.get_point` to get the initial point back. Example from points ------------------- @@ -264,4 +262,10 @@ Then, it computes the persistence diagram and displays it: :Requires: `Eigen `_ :math:`\geq` 3.1.0 and `CGAL `_ :math:`\geq` 4.11.0. A specific module for Alpha complex is available in 3d (cf. :class:`~gudhi.AlphaComplex3D`) and -allows to construct standard and weighted versions of alpha complexes. \ No newline at end of file +allows to construct standard and weighted versions of alpha complexes. + +Remark +"""""" + +* Contrary to the dD version, with the 3d version, the vertices in the output simplex tree are not guaranteed to match + the order of the input points. One can use :func:`~gudhi.AlphaComplex3D.get_point` to get the initial point back. diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index caebfab0..2cf4738b 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -36,22 +36,18 @@ cdef extern from "Alpha_complex_interface.h" namespace "Gudhi": # AlphaComplex python interface cdef class AlphaComplex: - """AlphaComplex is a simplicial complex constructed from the finite cells - of a Delaunay Triangulation. + """AlphaComplex is a simplicial complex constructed from the finite cells of a Delaunay Triangulation. - The filtration value of each simplex is computed as the square of the - circumradius of the simplex if the circumsphere is empty (the simplex is - then said to be Gabriel), and as the minimum of the filtration values of - the codimension 1 cofaces that make it not Gabriel otherwise. + The filtration value of each simplex is computed as the square of the circumradius of the simplex if the + circumsphere is empty (the simplex is then said to be Gabriel), and as the minimum of the filtration values of the + codimension 1 cofaces that make it not Gabriel otherwise. - All simplices that have a filtration value strictly greater than a given - alpha squared value are not inserted into the complex. + All simplices that have a filtration value strictly greater than a given alpha squared value are not inserted into + the complex. .. note:: - When Alpha_complex is constructed with an infinite value of alpha, the - complex is a Delaunay complex. - + When Alpha_complex is constructed with an infinite value of alpha, the complex is a Delaunay complex. """ cdef Alpha_complex_interface * this_ptr @@ -63,18 +59,14 @@ cdef class AlphaComplex: :param points: A list of points in d-Dimension. :type points: Iterable[Iterable[float]] - :param off_file: **[deprecated]** An `OFF file style `_ - name. - If an `off_file` is given with `points` as arguments, only points from the file are - taken into account. + :param off_file: **[deprecated]** An `OFF file style `_ name. + If an `off_file` is given with `points` as arguments, only points from the file are taken into account. :type off_file: string - :param weights: A list of weights. If set, the number of weights must correspond to the - number of points. + :param weights: A list of weights. If set, the number of weights must correspond to the number of points. :type weights: Iterable[float] - :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is - 'safe'. + :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is 'safe'. :type precision: string :raises FileNotFoundError: **[deprecated]** If `off_file` is set but not found. @@ -83,8 +75,7 @@ cdef class AlphaComplex: # The real cython constructor def __cinit__(self, points = [], off_file = '', weights=None, precision = 'safe'): - assert precision in ['fast', 'safe', 'exact'], \ - "Alpha complex precision can only be 'fast', 'safe' or 'exact'" + assert precision in ['fast', 'safe', 'exact'], "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' diff --git a/src/python/gudhi/alpha_complex_3d.pyx b/src/python/gudhi/alpha_complex_3d.pyx index 3959004a..4d3fe59c 100644 --- a/src/python/gudhi/alpha_complex_3d.pyx +++ b/src/python/gudhi/alpha_complex_3d.pyx @@ -36,22 +36,24 @@ cdef extern from "Alpha_complex_interface_3d.h" namespace "Gudhi": # AlphaComplex3D python interface cdef class AlphaComplex3D: - """AlphaComplex3D is a simplicial complex constructed from the finite cells - of a Delaunay Triangulation. + """AlphaComplex3D is a simplicial complex constructed from the finite cells of a Delaunay Triangulation. - The filtration value of each simplex is computed as the square of the - circumradius of the simplex if the circumsphere is empty (the simplex is - then said to be Gabriel), and as the minimum of the filtration values of - the codimension 1 cofaces that make it not Gabriel otherwise. + The filtration value of each simplex is computed as the square of the circumradius of the simplex if the + circumsphere is empty (the simplex is then said to be Gabriel), and as the minimum of the filtration values of the + codimension 1 cofaces that make it not Gabriel otherwise. - All simplices that have a filtration value strictly greater than a given - alpha squared value are not inserted into the complex. + All simplices that have a filtration value strictly greater than a given alpha squared value are not inserted into + the complex. .. note:: - When AlphaComplex3D is constructed with an infinite value of alpha, the - complex is a Delaunay complex. + When AlphaComplex3D is constructed with an infinite value of alpha, the complex is a Delaunay complex. + .. warning:: + + Contrary to the dD version, with the 3d version, the vertices in the output simplex tree are not guaranteed to + match the order of the input points. One can use :func:`~gudhi.AlphaComplex3D.get_point` to get the initial + point back. """ cdef Alpha_complex_interface_3d * this_ptr @@ -63,12 +65,10 @@ cdef class AlphaComplex3D: :param points: A list of points in d-Dimension. :type points: Iterable[Iterable[float]] - :param weights: A list of weights. If set, the number of weights must correspond to the - number of points. + :param weights: A list of weights. If set, the number of weights must correspond to the number of points. :type weights: Iterable[float] - :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is - 'safe'. + :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is 'safe'. :type precision: string :raises ValueError: In case of inconsistency between the number of points and weights. @@ -76,8 +76,7 @@ cdef class AlphaComplex3D: # The real cython constructor def __cinit__(self, points = [], weights=[], precision = 'safe'): - assert precision in ['fast', 'safe', 'exact'], \ - "Alpha complex precision can only be 'fast', 'safe' or 'exact'" + assert precision in ['fast', 'safe', 'exact'], "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' -- cgit v1.2.3 From 6d7c79a352023dd380b7361057cb7db371c5d269 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 22 Jun 2021 17:34:27 +0200 Subject: Fix #448. Add Alpha complex 3d python module unitary tests accordingly. --- src/Alpha_complex/include/gudhi/Alpha_complex_3d.h | 19 ++- .../test/Alpha_complex_3d_unit_test.cpp | 30 +++++ .../test/Weighted_alpha_complex_unit_test.cpp | 2 +- src/python/gudhi/alpha_complex_3d.pyx | 4 +- src/python/test/test_alpha_complex.py | 107 ++++++--------- src/python/test/test_alpha_complex_3d.py | 149 +++++++++++++++++++++ 6 files changed, 231 insertions(+), 80 deletions(-) create mode 100755 src/python/test/test_alpha_complex_3d.py (limited to 'src/python/gudhi') diff --git a/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h b/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h index 4e5fc933..73f9dd41 100644 --- a/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h +++ b/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h @@ -48,6 +48,7 @@ #include // for std::unique_ptr #include // for std::conditional and std::enable_if #include // for numeric_limits<> +#include // for domain_error and invalid_argument // Make compilation fail - required for external projects - https://github.com/GUDHI/gudhi-devel/issues/10 #if CGAL_VERSION_NR < 1041101000 @@ -428,19 +429,18 @@ Weighted_alpha_complex_3d::Weighted_point_3 wp0(Weighted_alpha_complex_3d::Bare_ * @param[in] max_alpha_square maximum for alpha square value. Default value is +\f$\infty\f$, and there is very * little point using anything else since it does not save time. * - * @return true if creation succeeds, false otherwise. + * @exception invalid_argument In debug mode, if `complex` given as argument is not empty. + * @exception domain_error If `points` given in the constructor are on a 2d plane. * * @pre The simplicial complex must be empty (no vertices). * */ template - bool create_complex(SimplicialComplexForAlpha3d& complex, + void create_complex(SimplicialComplexForAlpha3d& complex, Filtration_value max_alpha_square = std::numeric_limits::infinity()) { - if (complex.num_vertices() > 0) { - std::cerr << "Alpha_complex_3d create_complex - complex is not empty\n"; - return false; // ----- >> - } + GUDHI_CHECK(complex.num_vertices() == 0, + std::invalid_argument("Alpha_complex_3d create_complex - The complex given as argument is not empty")); using Complex_vertex_handle = typename SimplicialComplexForAlpha3d::Vertex_handle; using Simplex_tree_vector_vertex = std::vector; @@ -461,10 +461,8 @@ Weighted_alpha_complex_3d::Weighted_point_3 wp0(Weighted_alpha_complex_3d::Bare_ #ifdef DEBUG_TRACES std::clog << "filtration_with_alpha_values returns : " << objects.size() << " objects" << std::endl; #endif // DEBUG_TRACES - if (objects.size() == 0) { - std::cerr << "Alpha_complex_3d create_complex - no triangulation as points are on a 2d plane\n"; - return false; // ----- >> - } + if (objects.size() == 0) + throw std::domain_error("Alpha_complex_3d create_complex - no triangulation as points are on a 2d plane"); using Alpha_value_iterator = typename std::vector::const_iterator; Alpha_value_iterator alpha_value_iterator = alpha_values.begin(); @@ -559,7 +557,6 @@ Weighted_alpha_complex_3d::Weighted_point_3 wp0(Weighted_alpha_complex_3d::Bare_ // Remove all simplices that have a filtration value greater than max_alpha_square complex.prune_above_filtration(max_alpha_square); // -------------------------------------------------------------------------------------------- - return true; } /** \brief get_point returns the point corresponding to the vertex given as parameter. diff --git a/src/Alpha_complex/test/Alpha_complex_3d_unit_test.cpp b/src/Alpha_complex/test/Alpha_complex_3d_unit_test.cpp index a4ecb6ad..94021954 100644 --- a/src/Alpha_complex/test/Alpha_complex_3d_unit_test.cpp +++ b/src/Alpha_complex/test/Alpha_complex_3d_unit_test.cpp @@ -11,6 +11,7 @@ #define BOOST_TEST_DYN_LINK #define BOOST_TEST_MODULE "alpha_complex_3d" #include +#include #include // float comparison #include @@ -36,6 +37,7 @@ using Safe_alpha_complex_3d = using Exact_alpha_complex_3d = Gudhi::alpha_complex::Alpha_complex_3d; + template std::vector get_points() { std::vector points; @@ -197,3 +199,31 @@ BOOST_AUTO_TEST_CASE(Alpha_complex_3d_from_points) { ++safe_sh; } } + +typedef boost::mpl::list list_of_alpha_variants; + +BOOST_AUTO_TEST_CASE_TEMPLATE(Alpha_complex_3d_exceptions_points_on_plane, Alpha, list_of_alpha_variants) { + std::vector points_on_plane; + points_on_plane.emplace_back(1.0, 1.0 , 0.0); + points_on_plane.emplace_back(7.0, 0.0 , 0.0); + points_on_plane.emplace_back(4.0, 6.0 , 0.0); + points_on_plane.emplace_back(9.0, 6.0 , 0.0); + points_on_plane.emplace_back(0.0, 14.0, 0.0); + points_on_plane.emplace_back(2.0, 19.0, 0.0); + points_on_plane.emplace_back(9.0, 17.0, 0.0); + + Alpha alpha_complex(points_on_plane); + Gudhi::Simplex_tree<> stree; + + BOOST_CHECK_THROW(alpha_complex.create_complex(stree), std::domain_error); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(Alpha_complex_3d_exceptions_non_empty_simplex_tree, Alpha, list_of_alpha_variants) { + Alpha alpha_complex(get_points()); + Gudhi::Simplex_tree<> stree; + stree.insert_simplex_and_subfaces({2,1,0}, 3.0); + +#ifdef GUDHI_DEBUG + BOOST_CHECK_THROW(alpha_complex.create_complex(stree), std::invalid_argument); +#endif // GUDHI_DEBUG +} \ No newline at end of file diff --git a/src/Alpha_complex/test/Weighted_alpha_complex_unit_test.cpp b/src/Alpha_complex/test/Weighted_alpha_complex_unit_test.cpp index 875704ee..4e1a38df 100644 --- a/src/Alpha_complex/test/Weighted_alpha_complex_unit_test.cpp +++ b/src/Alpha_complex/test/Weighted_alpha_complex_unit_test.cpp @@ -83,7 +83,7 @@ BOOST_AUTO_TEST_CASE(Weighted_alpha_complex_3d_comparison) { // Weighted alpha complex for 3D version Exact_weighted_alpha_complex_3d alpha_complex_3D_from_weighted_points(w_points_3); Gudhi::Simplex_tree<> w_simplex_3; - BOOST_CHECK(alpha_complex_3D_from_weighted_points.create_complex(w_simplex_3)); + alpha_complex_3D_from_weighted_points.create_complex(w_simplex_3); std::clog << "Iterator on weighted alpha complex 3D simplices in the filtration order, with [filtration value]:" << std::endl; diff --git a/src/python/gudhi/alpha_complex_3d.pyx b/src/python/gudhi/alpha_complex_3d.pyx index 4d3fe59c..40f1b43a 100644 --- a/src/python/gudhi/alpha_complex_3d.pyx +++ b/src/python/gudhi/alpha_complex_3d.pyx @@ -62,7 +62,7 @@ cdef class AlphaComplex3D: def __init__(self, points=[], weights=[], precision='safe'): """AlphaComplex3D constructor. - :param points: A list of points in d-Dimension. + :param points: A list of points in 3d. :type points: Iterable[Iterable[float]] :param weights: A list of weights. If set, the number of weights must correspond to the number of points. @@ -118,6 +118,8 @@ cdef class AlphaComplex3D: :type max_alpha_square: float :returns: A simplex tree created from the Delaunay Triangulation. :rtype: SimplexTree + + :raises ValueError: If the points given at construction time are on a plane. """ stree = SimplexTree() cdef double mas = max_alpha_square diff --git a/src/python/test/test_alpha_complex.py b/src/python/test/test_alpha_complex.py index b0f219e1..f15284f3 100755 --- a/src/python/test/test_alpha_complex.py +++ b/src/python/test/test_alpha_complex.py @@ -8,7 +8,7 @@ - YYYY/MM Author: Description of the modification """ -import gudhi as gd +from gudhi import AlphaComplex import math import numpy as np import pytest @@ -21,17 +21,14 @@ except ImportError: # python2 from itertools import izip_longest as zip_longest -__author__ = "Vincent Rouvreau" -__copyright__ = "Copyright (C) 2016 Inria" -__license__ = "MIT" def _empty_alpha(precision): - alpha_complex = gd.AlphaComplex(precision = precision) + alpha_complex = AlphaComplex(precision = precision) assert alpha_complex.__is_defined() == True def _one_2d_point_alpha(precision): - alpha_complex = gd.AlphaComplex(points=[[0, 0]], precision = precision) + alpha_complex = AlphaComplex(points=[[0, 0]], precision = precision) assert alpha_complex.__is_defined() == True def test_empty_alpha(): @@ -41,7 +38,7 @@ def test_empty_alpha(): def _infinite_alpha(precision): point_list = [[0, 0], [1, 0], [0, 1], [1, 1]] - alpha_complex = gd.AlphaComplex(points=point_list, precision = precision) + alpha_complex = AlphaComplex(points=point_list, precision = precision) assert alpha_complex.__is_defined() == True simplex_tree = alpha_complex.create_simplex_tree() @@ -76,18 +73,9 @@ def _infinite_alpha(precision): assert point_list[1] == alpha_complex.get_point(1) assert point_list[2] == alpha_complex.get_point(2) assert point_list[3] == alpha_complex.get_point(3) - try: - alpha_complex.get_point(4) == [] - except IndexError: - pass - else: - assert False - try: - alpha_complex.get_point(125) == [] - except IndexError: - pass - else: - assert False + + with pytest.raises(IndexError): + alpha_complex.get_point(len(point_list)) def test_infinite_alpha(): for precision in ['fast', 'safe', 'exact']: @@ -95,7 +83,7 @@ def test_infinite_alpha(): def _filtered_alpha(precision): point_list = [[0, 0], [1, 0], [0, 1], [1, 1]] - filtered_alpha = gd.AlphaComplex(points=point_list, precision = precision) + filtered_alpha = AlphaComplex(points=point_list, precision = precision) simplex_tree = filtered_alpha.create_simplex_tree(max_alpha_square=0.25) @@ -106,18 +94,9 @@ def _filtered_alpha(precision): assert point_list[1] == filtered_alpha.get_point(1) assert point_list[2] == filtered_alpha.get_point(2) assert point_list[3] == filtered_alpha.get_point(3) - try: - filtered_alpha.get_point(4) == [] - except IndexError: - pass - else: - assert False - try: - filtered_alpha.get_point(125) == [] - except IndexError: - pass - else: - assert False + + with pytest.raises(IndexError): + filtered_alpha.get_point(len(point_list)) assert list(simplex_tree.get_filtration()) == [ ([0], 0.0), @@ -148,10 +127,10 @@ def _safe_alpha_persistence_comparison(precision): embedding2 = [[signal[i], delayed[i]] for i in range(len(time))] #build alpha complex and simplex tree - alpha_complex1 = gd.AlphaComplex(points=embedding1, precision = precision) + alpha_complex1 = AlphaComplex(points=embedding1, precision = precision) simplex_tree1 = alpha_complex1.create_simplex_tree() - alpha_complex2 = gd.AlphaComplex(points=embedding2, precision = precision) + alpha_complex2 = AlphaComplex(points=embedding2, precision = precision) simplex_tree2 = alpha_complex2.create_simplex_tree() diag1 = simplex_tree1.persistence() @@ -169,7 +148,7 @@ def test_safe_alpha_persistence_comparison(): def _delaunay_complex(precision): point_list = [[0, 0], [1, 0], [0, 1], [1, 1]] - filtered_alpha = gd.AlphaComplex(points=point_list, precision = precision) + filtered_alpha = AlphaComplex(points=point_list, precision = precision) simplex_tree = filtered_alpha.create_simplex_tree(default_filtration_value = True) @@ -180,18 +159,11 @@ def _delaunay_complex(precision): assert point_list[1] == filtered_alpha.get_point(1) assert point_list[2] == filtered_alpha.get_point(2) assert point_list[3] == filtered_alpha.get_point(3) - try: - filtered_alpha.get_point(4) == [] - except IndexError: - pass - else: - assert False - try: - filtered_alpha.get_point(125) == [] - except IndexError: - pass - else: - assert False + + with pytest.raises(IndexError): + filtered_alpha.get_point(4) + with pytest.raises(IndexError): + filtered_alpha.get_point(125) for filtered_value in simplex_tree.get_filtration(): assert math.isnan(filtered_value[1]) @@ -205,7 +177,7 @@ def test_delaunay_complex(): _delaunay_complex(precision) def _3d_points_on_a_plane(precision, default_filtration_value): - alpha = gd.AlphaComplex(points = [[1.0, 1.0 , 0.0], + alpha = AlphaComplex(points = [[1.0, 1.0 , 0.0], [7.0, 0.0 , 0.0], [4.0, 6.0 , 0.0], [9.0, 6.0 , 0.0], @@ -225,10 +197,10 @@ def test_3d_points_on_a_plane(): def _3d_tetrahedrons(precision): points = 10*np.random.rand(10, 3) - alpha = gd.AlphaComplex(points = points, precision = precision) + alpha = AlphaComplex(points = points, precision = precision) st_alpha = alpha.create_simplex_tree(default_filtration_value = False) # New AlphaComplex for get_point to work - delaunay = gd.AlphaComplex(points = points, precision = precision) + delaunay = AlphaComplex(points = points, precision = precision) st_delaunay = delaunay.create_simplex_tree(default_filtration_value = True) delaunay_tetra = [] @@ -272,11 +244,12 @@ def test_off_file_deprecation_warning(): off_file.close() with pytest.warns(DeprecationWarning): - alpha = gd.AlphaComplex(off_file="alphacomplexdoc.off") + alpha = AlphaComplex(off_file="alphacomplexdoc.off") def test_non_existing_off_file(): - with pytest.raises(FileNotFoundError): - alpha = gd.AlphaComplex(off_file="pouetpouettralala.toubiloubabdou") + with pytest.warns(DeprecationWarning): + with pytest.raises(FileNotFoundError): + alpha = AlphaComplex(off_file="pouetpouettralala.toubiloubabdou") def test_inconsistency_points_and_weights(): points = [[1.0, 1.0 , 0.0], @@ -288,27 +261,27 @@ def test_inconsistency_points_and_weights(): [9.0, 17.0, 0.0]] with pytest.raises(ValueError): # 7 points, 8 weights, on purpose - alpha = gd.AlphaComplex(points = points, + alpha = AlphaComplex(points = points, weights = [1., 2., 3., 4., 5., 6., 7., 8.]) with pytest.raises(ValueError): # 7 points, 6 weights, on purpose - alpha = gd.AlphaComplex(points = points, + alpha = AlphaComplex(points = points, weights = [1., 2., 3., 4., 5., 6.]) def _weighted_doc_example(precision): - stree_from_values = gd.AlphaComplex(points=[[ 1., -1., -1.], - [-1., 1., -1.], - [-1., -1., 1.], - [ 1., 1., 1.], - [ 2., 2., 2.]], - weights = [4., 4., 4., 4., 1.], - precision = precision).create_simplex_tree() - - assert stree_from_values.filtration([0, 1, 2, 3]) == pytest.approx(-1.) - assert stree_from_values.filtration([0, 1, 3, 4]) == pytest.approx(95.) - assert stree_from_values.filtration([0, 2, 3, 4]) == pytest.approx(95.) - assert stree_from_values.filtration([1, 2, 3, 4]) == pytest.approx(95.) + stree = AlphaComplex(points=[[ 1., -1., -1.], + [-1., 1., -1.], + [-1., -1., 1.], + [ 1., 1., 1.], + [ 2., 2., 2.]], + weights = [4., 4., 4., 4., 1.], + precision = precision).create_simplex_tree() + + assert stree.filtration([0, 1, 2, 3]) == pytest.approx(-1.) + assert stree.filtration([0, 1, 3, 4]) == pytest.approx(95.) + assert stree.filtration([0, 2, 3, 4]) == pytest.approx(95.) + assert stree.filtration([1, 2, 3, 4]) == pytest.approx(95.) def test_weighted_doc_example(): for precision in ['fast', 'safe', 'exact']: diff --git a/src/python/test/test_alpha_complex_3d.py b/src/python/test/test_alpha_complex_3d.py new file mode 100755 index 00000000..f7bd4fda --- /dev/null +++ b/src/python/test/test_alpha_complex_3d.py @@ -0,0 +1,149 @@ +""" 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): Vincent Rouvreau + + Copyright (C) 2021 Inria + + Modification(s): + - YYYY/MM Author: Description of the modification +""" + +from gudhi import AlphaComplex3D +import pytest + +try: + # python3 + from itertools import zip_longest +except ImportError: + # python2 + from itertools import izip_longest as zip_longest + + + +def _empty_alpha(precision): + alpha_complex = AlphaComplex3D(precision = precision) + assert alpha_complex.__is_defined() == True + +def _one_3d_point_alpha(precision): + alpha_complex = AlphaComplex3D(points=[[0, 0, 0]], precision = precision) + assert alpha_complex.__is_defined() == True + +def test_empty_alpha(): + for precision in ['fast', 'safe', 'exact']: + _empty_alpha(precision) + _one_3d_point_alpha(precision) + +def _infinite_alpha(precision): + point_list = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]] + alpha_complex = AlphaComplex3D(points=point_list, precision = precision) + assert alpha_complex.__is_defined() == True + + stree = alpha_complex.create_simplex_tree() + assert stree.__is_persistence_defined() == False + + assert stree.num_simplices() == 51 + assert stree.num_vertices() == len(point_list) + + for filtration in stree.get_filtration(): + if len(filtration[0]) == 1: + assert filtration[1] == 0. + if len(filtration[0]) == 4: + assert filtration[1] == 0.75 + + for idx in range(len(point_list)): + pt_idx = point_list.index(alpha_complex.get_point(idx)) + assert pt_idx >= 0 + assert pt_idx < len(point_list) + + with pytest.raises(IndexError): + alpha_complex.get_point(len(point_list)) + +def test_infinite_alpha(): + for precision in ['fast', 'safe', 'exact']: + _infinite_alpha(precision) + +def _filtered_alpha(precision): + point_list = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]] + filtered_alpha = AlphaComplex3D(points=point_list, precision = precision) + + stree = filtered_alpha.create_simplex_tree(max_alpha_square=0.25) + + assert stree.num_simplices() == 20 + assert stree.num_vertices() == len(point_list) + + for filtration in stree.get_filtration(): + if len(filtration[0]) == 1: + assert filtration[1] == 0. + elif len(filtration[0]) == 2: + assert filtration[1] == 0.25 + else: + assert False + + for idx in range(len(point_list)): + pt_idx = point_list.index(filtered_alpha.get_point(idx)) + assert pt_idx >= 0 + assert pt_idx < len(point_list) + + with pytest.raises(IndexError): + filtered_alpha.get_point(len(point_list)) + +def test_filtered_alpha(): + for precision in ['fast', 'safe', 'exact']: + _filtered_alpha(precision) + +def _3d_points_on_a_plane(precision): + alpha = AlphaComplex3D(points = [[1.0, 1.0 , 0.0], + [7.0, 0.0 , 0.0], + [4.0, 6.0 , 0.0], + [9.0, 6.0 , 0.0], + [0.0, 14.0, 0.0], + [2.0, 19.0, 0.0], + [9.0, 17.0, 0.0]], precision = precision) + + with pytest.raises(ValueError): + stree = alpha.create_simplex_tree() + +def test_3d_points_on_a_plane(): + for precision in ['fast', 'safe', 'exact']: + _3d_points_on_a_plane(precision) + +def test_inconsistency_points_and_weights(): + points = [[1.0, 1.0 , 1.0], + [7.0, 0.0 , 2.0], + [4.0, 6.0 , 0.0], + [9.0, 6.0 , 1.0], + [0.0, 14.0, 2.0], + [2.0, 19.0, 0.0], + [9.0, 17.0, 1.0]] + with pytest.raises(ValueError): + # 7 points, 8 weights, on purpose + alpha = AlphaComplex3D(points = points, + weights = [1., 2., 3., 4., 5., 6., 7., 8.]) + + with pytest.raises(ValueError): + # 7 points, 6 weights, on purpose + alpha = AlphaComplex3D(points = points, + weights = [1., 2., 3., 4., 5., 6.]) + +def _weighted_doc_example(precision): + pts = [[ 1., -1., -1.], + [-1., 1., -1.], + [-1., -1., 1.], + [ 1., 1., 1.], + [ 2., 2., 2.]] + wgts = [4., 4., 4., 4., 1.] + alpha = AlphaComplex3D(points = pts, weights = wgts, precision = precision) + stree = alpha.create_simplex_tree() + + # Needs to retrieve points as points are shuffled + get_idx = lambda idx: pts.index(alpha.get_point(idx)) + indices = [get_idx(x) for x in range(len(pts))] + + assert stree.filtration([indices[x] for x in [0, 1, 2, 3]]) == pytest.approx(-1.) + assert stree.filtration([indices[x] for x in [0, 1, 3, 4]]) == pytest.approx(95.) + assert stree.filtration([indices[x] for x in [0, 2, 3, 4]]) == pytest.approx(95.) + assert stree.filtration([indices[x] for x in [1, 2, 3, 4]]) == pytest.approx(95.) + +def test_weighted_doc_example(): + for precision in ['fast', 'safe', 'exact']: + _weighted_doc_example(precision) -- cgit v1.2.3 From 1da601457d00dfe951c89ee97b6f8053e2699c78 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 22 Jun 2021 17:48:26 +0200 Subject: Add exception when input points not in 3d --- src/python/gudhi/alpha_complex_3d.pyx | 5 +++++ src/python/test/test_alpha_complex_3d.py | 10 ++++++++++ 2 files changed, 15 insertions(+) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex_3d.pyx b/src/python/gudhi/alpha_complex_3d.pyx index 40f1b43a..578011a7 100644 --- a/src/python/gudhi/alpha_complex_3d.pyx +++ b/src/python/gudhi/alpha_complex_3d.pyx @@ -71,6 +71,7 @@ cdef class AlphaComplex3D: :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is 'safe'. :type precision: string + :raises ValueError: If the given points are not in 3d. :raises ValueError: In case of inconsistency between the number of points and weights. """ @@ -80,6 +81,10 @@ cdef class AlphaComplex3D: cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' + if len(points) > 0: + if len(points[0]) != 3: + raise ValueError("AlphaComplex3D only accepts 3d points as an input") + # weights are set but is inconsistent with the number of points if len(weights) != 0 and len(weights) != len(points): raise ValueError("Inconsistency between the number of points and weights") diff --git a/src/python/test/test_alpha_complex_3d.py b/src/python/test/test_alpha_complex_3d.py index f7bd4fda..a5d9373b 100755 --- a/src/python/test/test_alpha_complex_3d.py +++ b/src/python/test/test_alpha_complex_3d.py @@ -10,6 +10,7 @@ from gudhi import AlphaComplex3D import pytest +import numpy as np try: # python3 @@ -147,3 +148,12 @@ def _weighted_doc_example(precision): def test_weighted_doc_example(): for precision in ['fast', 'safe', 'exact']: _weighted_doc_example(precision) + +def test_points_not_in_3d(): + with pytest.raises(ValueError): + alpha = AlphaComplex3D(points = np.random.rand(6,2)) + with pytest.raises(ValueError): + alpha = AlphaComplex3D(points = np.random.rand(6,4)) + + alpha = AlphaComplex3D(points = np.random.rand(6,3)) + assert alpha.__is_defined() == True \ No newline at end of file -- cgit v1.2.3 From 62b63fd55442b152b934dc0c9ed662970ddb32dc Mon Sep 17 00:00:00 2001 From: Hind-M 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/gudhi') 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: Thu, 1 Jul 2021 17:30:38 +0200 Subject: Revert AlphaComplex3D. To be done with periodic --- .../example/Alpha_complex_3d_from_points.cpp | 29 ++-- .../Weighted_alpha_complex_3d_from_points.cpp | 29 ++-- src/Alpha_complex/include/gudhi/Alpha_complex_3d.h | 19 +-- .../test/Alpha_complex_3d_unit_test.cpp | 30 ---- .../test/Weighted_alpha_complex_unit_test.cpp | 2 +- src/python/CMakeLists.txt | 2 - src/python/doc/alpha_complex_ref.rst | 6 - src/python/doc/alpha_complex_user.rst | 14 -- src/python/gudhi/alpha_complex_3d.pyx | 135 ----------------- src/python/include/Alpha_complex_factory.h | 35 ----- src/python/test/test_alpha_complex_3d.py | 159 --------------------- 11 files changed, 42 insertions(+), 418 deletions(-) delete mode 100644 src/python/gudhi/alpha_complex_3d.pyx delete mode 100755 src/python/test/test_alpha_complex_3d.py (limited to 'src/python/gudhi') diff --git a/src/Alpha_complex/example/Alpha_complex_3d_from_points.cpp b/src/Alpha_complex/example/Alpha_complex_3d_from_points.cpp index dd3c0225..a2c85138 100644 --- a/src/Alpha_complex/example/Alpha_complex_3d_from_points.cpp +++ b/src/Alpha_complex/example/Alpha_complex_3d_from_points.cpp @@ -34,22 +34,23 @@ int main(int argc, char **argv) { Alpha_complex_3d alpha_complex_from_points(points); Gudhi::Simplex_tree<> simplex; - alpha_complex_from_points.create_complex(simplex); - // ---------------------------------------------------------------------------- - // Display information about the alpha complex - // ---------------------------------------------------------------------------- - std::clog << "Alpha complex is of dimension " << simplex.dimension() << " - " << simplex.num_simplices() - << " simplices - " << simplex.num_vertices() << " vertices." << std::endl; + if (alpha_complex_from_points.create_complex(simplex)) { + // ---------------------------------------------------------------------------- + // Display information about the alpha complex + // ---------------------------------------------------------------------------- + std::clog << "Alpha complex is of dimension " << simplex.dimension() << " - " << simplex.num_simplices() + << " simplices - " << simplex.num_vertices() << " vertices." << std::endl; - std::clog << "Iterator on alpha complex simplices in the filtration order, with [filtration value]:" << std::endl; - for (auto f_simplex : simplex.filtration_simplex_range()) { - std::clog << " ( "; - for (auto vertex : simplex.simplex_vertex_range(f_simplex)) { - std::clog << vertex << " "; + std::clog << "Iterator on alpha complex simplices in the filtration order, with [filtration value]:" << std::endl; + for (auto f_simplex : simplex.filtration_simplex_range()) { + std::clog << " ( "; + for (auto vertex : simplex.simplex_vertex_range(f_simplex)) { + std::clog << vertex << " "; + } + std::clog << ") -> " + << "[" << simplex.filtration(f_simplex) << "] "; + std::clog << std::endl; } - std::clog << ") -> " - << "[" << simplex.filtration(f_simplex) << "] "; - std::clog << std::endl; } return 0; } diff --git a/src/Alpha_complex/example/Weighted_alpha_complex_3d_from_points.cpp b/src/Alpha_complex/example/Weighted_alpha_complex_3d_from_points.cpp index 507d6413..ee12d418 100644 --- a/src/Alpha_complex/example/Weighted_alpha_complex_3d_from_points.cpp +++ b/src/Alpha_complex/example/Weighted_alpha_complex_3d_from_points.cpp @@ -30,22 +30,23 @@ int main(int argc, char **argv) { Weighted_alpha_complex_3d alpha_complex_from_points(weighted_points); Gudhi::Simplex_tree<> simplex; - alpha_complex_from_points.create_complex(simplex); - // ---------------------------------------------------------------------------- - // Display information about the alpha complex - // ---------------------------------------------------------------------------- - std::clog << "Weighted alpha complex is of dimension " << simplex.dimension() << " - " << simplex.num_simplices() - << " simplices - " << simplex.num_vertices() << " vertices." << std::endl; + if (alpha_complex_from_points.create_complex(simplex)) { + // ---------------------------------------------------------------------------- + // Display information about the alpha complex + // ---------------------------------------------------------------------------- + std::clog << "Weighted alpha complex is of dimension " << simplex.dimension() << " - " << simplex.num_simplices() + << " simplices - " << simplex.num_vertices() << " vertices." << std::endl; - std::clog << "Iterator on weighted alpha complex simplices in the filtration order, with [filtration value]:" << std::endl; - for (auto f_simplex : simplex.filtration_simplex_range()) { - std::clog << " ( "; - for (auto vertex : simplex.simplex_vertex_range(f_simplex)) { - std::clog << vertex << " "; + std::clog << "Iterator on weighted alpha complex simplices in the filtration order, with [filtration value]:" << std::endl; + for (auto f_simplex : simplex.filtration_simplex_range()) { + std::clog << " ( "; + for (auto vertex : simplex.simplex_vertex_range(f_simplex)) { + std::clog << vertex << " "; + } + std::clog << ") -> " + << "[" << simplex.filtration(f_simplex) << "] "; + std::clog << std::endl; } - std::clog << ") -> " - << "[" << simplex.filtration(f_simplex) << "] "; - std::clog << std::endl; } return 0; } diff --git a/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h b/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h index 73f9dd41..4e5fc933 100644 --- a/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h +++ b/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h @@ -48,7 +48,6 @@ #include // for std::unique_ptr #include // for std::conditional and std::enable_if #include // for numeric_limits<> -#include // for domain_error and invalid_argument // Make compilation fail - required for external projects - https://github.com/GUDHI/gudhi-devel/issues/10 #if CGAL_VERSION_NR < 1041101000 @@ -429,18 +428,19 @@ Weighted_alpha_complex_3d::Weighted_point_3 wp0(Weighted_alpha_complex_3d::Bare_ * @param[in] max_alpha_square maximum for alpha square value. Default value is +\f$\infty\f$, and there is very * little point using anything else since it does not save time. * - * @exception invalid_argument In debug mode, if `complex` given as argument is not empty. - * @exception domain_error If `points` given in the constructor are on a 2d plane. + * @return true if creation succeeds, false otherwise. * * @pre The simplicial complex must be empty (no vertices). * */ template - void create_complex(SimplicialComplexForAlpha3d& complex, + bool create_complex(SimplicialComplexForAlpha3d& complex, Filtration_value max_alpha_square = std::numeric_limits::infinity()) { - GUDHI_CHECK(complex.num_vertices() == 0, - std::invalid_argument("Alpha_complex_3d create_complex - The complex given as argument is not empty")); + if (complex.num_vertices() > 0) { + std::cerr << "Alpha_complex_3d create_complex - complex is not empty\n"; + return false; // ----- >> + } using Complex_vertex_handle = typename SimplicialComplexForAlpha3d::Vertex_handle; using Simplex_tree_vector_vertex = std::vector; @@ -461,8 +461,10 @@ Weighted_alpha_complex_3d::Weighted_point_3 wp0(Weighted_alpha_complex_3d::Bare_ #ifdef DEBUG_TRACES std::clog << "filtration_with_alpha_values returns : " << objects.size() << " objects" << std::endl; #endif // DEBUG_TRACES - if (objects.size() == 0) - throw std::domain_error("Alpha_complex_3d create_complex - no triangulation as points are on a 2d plane"); + if (objects.size() == 0) { + std::cerr << "Alpha_complex_3d create_complex - no triangulation as points are on a 2d plane\n"; + return false; // ----- >> + } using Alpha_value_iterator = typename std::vector::const_iterator; Alpha_value_iterator alpha_value_iterator = alpha_values.begin(); @@ -557,6 +559,7 @@ Weighted_alpha_complex_3d::Weighted_point_3 wp0(Weighted_alpha_complex_3d::Bare_ // Remove all simplices that have a filtration value greater than max_alpha_square complex.prune_above_filtration(max_alpha_square); // -------------------------------------------------------------------------------------------- + return true; } /** \brief get_point returns the point corresponding to the vertex given as parameter. diff --git a/src/Alpha_complex/test/Alpha_complex_3d_unit_test.cpp b/src/Alpha_complex/test/Alpha_complex_3d_unit_test.cpp index 94021954..a4ecb6ad 100644 --- a/src/Alpha_complex/test/Alpha_complex_3d_unit_test.cpp +++ b/src/Alpha_complex/test/Alpha_complex_3d_unit_test.cpp @@ -11,7 +11,6 @@ #define BOOST_TEST_DYN_LINK #define BOOST_TEST_MODULE "alpha_complex_3d" #include -#include #include // float comparison #include @@ -37,7 +36,6 @@ using Safe_alpha_complex_3d = using Exact_alpha_complex_3d = Gudhi::alpha_complex::Alpha_complex_3d; - template std::vector get_points() { std::vector points; @@ -199,31 +197,3 @@ BOOST_AUTO_TEST_CASE(Alpha_complex_3d_from_points) { ++safe_sh; } } - -typedef boost::mpl::list list_of_alpha_variants; - -BOOST_AUTO_TEST_CASE_TEMPLATE(Alpha_complex_3d_exceptions_points_on_plane, Alpha, list_of_alpha_variants) { - std::vector points_on_plane; - points_on_plane.emplace_back(1.0, 1.0 , 0.0); - points_on_plane.emplace_back(7.0, 0.0 , 0.0); - points_on_plane.emplace_back(4.0, 6.0 , 0.0); - points_on_plane.emplace_back(9.0, 6.0 , 0.0); - points_on_plane.emplace_back(0.0, 14.0, 0.0); - points_on_plane.emplace_back(2.0, 19.0, 0.0); - points_on_plane.emplace_back(9.0, 17.0, 0.0); - - Alpha alpha_complex(points_on_plane); - Gudhi::Simplex_tree<> stree; - - BOOST_CHECK_THROW(alpha_complex.create_complex(stree), std::domain_error); -} - -BOOST_AUTO_TEST_CASE_TEMPLATE(Alpha_complex_3d_exceptions_non_empty_simplex_tree, Alpha, list_of_alpha_variants) { - Alpha alpha_complex(get_points()); - Gudhi::Simplex_tree<> stree; - stree.insert_simplex_and_subfaces({2,1,0}, 3.0); - -#ifdef GUDHI_DEBUG - BOOST_CHECK_THROW(alpha_complex.create_complex(stree), std::invalid_argument); -#endif // GUDHI_DEBUG -} \ No newline at end of file diff --git a/src/Alpha_complex/test/Weighted_alpha_complex_unit_test.cpp b/src/Alpha_complex/test/Weighted_alpha_complex_unit_test.cpp index 4e1a38df..875704ee 100644 --- a/src/Alpha_complex/test/Weighted_alpha_complex_unit_test.cpp +++ b/src/Alpha_complex/test/Weighted_alpha_complex_unit_test.cpp @@ -83,7 +83,7 @@ BOOST_AUTO_TEST_CASE(Weighted_alpha_complex_3d_comparison) { // Weighted alpha complex for 3D version Exact_weighted_alpha_complex_3d alpha_complex_3D_from_weighted_points(w_points_3); Gudhi::Simplex_tree<> w_simplex_3; - alpha_complex_3D_from_weighted_points.create_complex(w_simplex_3); + BOOST_CHECK(alpha_complex_3D_from_weighted_points.create_complex(w_simplex_3)); std::clog << "Iterator on weighted alpha complex 3D simplices in the filtration order, with [filtration value]:" << std::endl; diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 669239b8..bfa78131 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -62,7 +62,6 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'subsampling', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'tangential_complex', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'alpha_complex', ") - set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'alpha_complex_3d', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'euclidean_witness_complex', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'euclidean_strong_witness_complex', ") # Modules that should not be auto-imported in __init__.py @@ -158,7 +157,6 @@ if(PYTHONINTERP_FOUND) set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'nerve_gic', ") endif () if (NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) - set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'alpha_complex_3d', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'subsampling', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'tangential_complex', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'euclidean_witness_complex', ") diff --git a/src/python/doc/alpha_complex_ref.rst b/src/python/doc/alpha_complex_ref.rst index 49321368..eaa72551 100644 --- a/src/python/doc/alpha_complex_ref.rst +++ b/src/python/doc/alpha_complex_ref.rst @@ -11,9 +11,3 @@ Alpha complex reference manual :undoc-members: .. automethod:: gudhi.AlphaComplex.__init__ - -.. autoclass:: gudhi.AlphaComplex3D - :members: - :undoc-members: - - .. automethod:: gudhi.AlphaComplex3D.__init__ diff --git a/src/python/doc/alpha_complex_user.rst b/src/python/doc/alpha_complex_user.rst index d7b09246..db0ccdc9 100644 --- a/src/python/doc/alpha_complex_user.rst +++ b/src/python/doc/alpha_complex_user.rst @@ -254,17 +254,3 @@ Then, it computes the persistence diagram and displays it: dgm = stree.persistence() gd.plot_persistence_diagram(dgm, legend = True) plt.show() - -3d specific version -^^^^^^^^^^^^^^^^^^^ - -:Requires: `Eigen `_ :math:`\geq` 3.1.0 and `CGAL `_ :math:`\geq` 4.11.0. - -A specific module for Alpha complex is available in 3d (cf. :class:`~gudhi.AlphaComplex3D`) and -allows to construct standard and weighted versions of alpha complexes. - -Remark -"""""" - -* Contrary to the dD version, with the 3d version, the vertices in the output simplex tree are not guaranteed to match - the order of the input points. One can use :func:`~gudhi.AlphaComplex3D.get_point` to get the initial point back. diff --git a/src/python/gudhi/alpha_complex_3d.pyx b/src/python/gudhi/alpha_complex_3d.pyx deleted file mode 100644 index 578011a7..00000000 --- a/src/python/gudhi/alpha_complex_3d.pyx +++ /dev/null @@ -1,135 +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): Vincent Rouvreau -# -# Copyright (C) 2021 Inria -# -# Modification(s): -# - YYYY/MM Author: Description of the modification - -from __future__ import print_function -from cython cimport numeric -from libcpp.vector cimport vector -from libcpp.utility cimport pair -from libcpp.string cimport string -from libcpp cimport bool -from libc.stdint cimport intptr_t -import errno -import os -import warnings - -from gudhi.simplex_tree cimport * -from gudhi.simplex_tree import SimplexTree -from gudhi import read_points_from_off_file - -__author__ = "Vincent Rouvreau" -__copyright__ = "Copyright (C) 2021 Inria" -__license__ = "GPL v3" - -cdef extern from "Alpha_complex_interface_3d.h" namespace "Gudhi": - cdef cppclass Alpha_complex_interface_3d "Gudhi::alpha_complex::Alpha_complex_interface_3d": - Alpha_complex_interface_3d(vector[vector[double]] points, vector[double] weights, bool fast_version, bool exact_version) nogil except + - vector[double] get_point(int vertex) nogil except + - void create_simplex_tree(Simplex_tree_interface_full_featured* simplex_tree, double max_alpha_square) nogil except + - -# AlphaComplex3D python interface -cdef class AlphaComplex3D: - """AlphaComplex3D is a simplicial complex constructed from the finite cells of a Delaunay Triangulation. - - The filtration value of each simplex is computed as the square of the circumradius of the simplex if the - circumsphere is empty (the simplex is then said to be Gabriel), and as the minimum of the filtration values of the - codimension 1 cofaces that make it not Gabriel otherwise. - - All simplices that have a filtration value strictly greater than a given alpha squared value are not inserted into - the complex. - - .. note:: - - When AlphaComplex3D is constructed with an infinite value of alpha, the complex is a Delaunay complex. - - .. warning:: - - Contrary to the dD version, with the 3d version, the vertices in the output simplex tree are not guaranteed to - match the order of the input points. One can use :func:`~gudhi.AlphaComplex3D.get_point` to get the initial - point back. - """ - - cdef Alpha_complex_interface_3d * this_ptr - - # Fake constructor that does nothing but documenting the constructor - def __init__(self, points=[], weights=[], precision='safe'): - """AlphaComplex3D constructor. - - :param points: A list of points in 3d. - :type points: Iterable[Iterable[float]] - - :param weights: A list of weights. If set, the number of weights must correspond to the number of points. - :type weights: Iterable[float] - - :param precision: Alpha complex precision can be 'fast', 'safe' or 'exact'. Default is 'safe'. - :type precision: string - - :raises ValueError: If the given points are not in 3d. - :raises ValueError: In case of inconsistency between the number of points and weights. - """ - - # The real cython constructor - def __cinit__(self, points = [], weights=[], precision = 'safe'): - assert precision in ['fast', 'safe', 'exact'], "Alpha complex precision can only be 'fast', 'safe' or 'exact'" - cdef bool fast = precision == 'fast' - cdef bool exact = precision == 'exact' - - if len(points) > 0: - if len(points[0]) != 3: - raise ValueError("AlphaComplex3D only accepts 3d points as an input") - - # weights are set but is inconsistent with the number of points - if len(weights) != 0 and len(weights) != len(points): - raise ValueError("Inconsistency between the number of points and weights") - - # need to copy the points to use them without the gil - cdef vector[vector[double]] pts - cdef vector[double] wgts - pts = points - wgts = weights - with nogil: - self.this_ptr = new Alpha_complex_interface_3d(pts, wgts, fast, exact) - - def __dealloc__(self): - if self.this_ptr != NULL: - del self.this_ptr - - def __is_defined(self): - """Returns true if AlphaComplex3D pointer is not NULL. - """ - return self.this_ptr != NULL - - def get_point(self, vertex): - """This function returns the point corresponding to a given vertex from the :class:`~gudhi.SimplexTree`. - - :param vertex: The vertex. - :type vertex: int - :rtype: list of float - :returns: the point. - """ - return self.this_ptr.get_point(vertex) - - def create_simplex_tree(self, max_alpha_square = float('inf')): - """ - :param max_alpha_square: The maximum alpha square threshold the simplices shall not exceed. Default is set to - infinity, and there is very little point using anything else since it does not save time. - :type max_alpha_square: float - :returns: A simplex tree created from the Delaunay Triangulation. - :rtype: SimplexTree - - :raises ValueError: If the points given at construction time are on a plane. - """ - stree = SimplexTree() - cdef double mas = max_alpha_square - cdef intptr_t stree_int_ptr=stree.thisptr - with nogil: - self.this_ptr.create_simplex_tree(stree_int_ptr, - mas) - return stree diff --git a/src/python/include/Alpha_complex_factory.h b/src/python/include/Alpha_complex_factory.h index fbbf8896..298469fe 100644 --- a/src/python/include/Alpha_complex_factory.h +++ b/src/python/include/Alpha_complex_factory.h @@ -147,41 +147,6 @@ class Inexact_alpha_complex_dD final : public Abstract_alpha_complex { Alpha_complex alpha_complex_; }; -template -class Alpha_complex_3D final : public Abstract_alpha_complex { - private: - using Bare_point = typename Alpha_complex_3d::Bare_point_3; - using Point = typename Alpha_complex_3d::Point_3; - - static Bare_point pt_cython_to_cgal_3(std::vector const& vec) { - return Bare_point(vec[0], vec[1], vec[2]); - } - - public: - Alpha_complex_3D(const std::vector>& points) - : alpha_complex_(boost::adaptors::transform(points, pt_cython_to_cgal_3)) { - } - - Alpha_complex_3D(const std::vector>& points, const std::vector& weights) - : alpha_complex_(boost::adaptors::transform(points, pt_cython_to_cgal_3), weights) { - } - - virtual std::vector get_point(int vh) override { - // Can be a Weighted or a Bare point in function of Weighted - return Point_cgal_to_cython()(alpha_complex_.get_point(vh)); - } - - virtual bool create_simplex_tree(Simplex_tree_interface<>* simplex_tree, double max_alpha_square, - bool default_filtration_value) override { - alpha_complex_.create_complex(*simplex_tree, max_alpha_square); - return true; - } - - private: - Alpha_complex_3d alpha_complex_; -}; - - } // namespace alpha_complex } // namespace Gudhi diff --git a/src/python/test/test_alpha_complex_3d.py b/src/python/test/test_alpha_complex_3d.py deleted file mode 100755 index a5d9373b..00000000 --- a/src/python/test/test_alpha_complex_3d.py +++ /dev/null @@ -1,159 +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): Vincent Rouvreau - - Copyright (C) 2021 Inria - - Modification(s): - - YYYY/MM Author: Description of the modification -""" - -from gudhi import AlphaComplex3D -import pytest -import numpy as np - -try: - # python3 - from itertools import zip_longest -except ImportError: - # python2 - from itertools import izip_longest as zip_longest - - - -def _empty_alpha(precision): - alpha_complex = AlphaComplex3D(precision = precision) - assert alpha_complex.__is_defined() == True - -def _one_3d_point_alpha(precision): - alpha_complex = AlphaComplex3D(points=[[0, 0, 0]], precision = precision) - assert alpha_complex.__is_defined() == True - -def test_empty_alpha(): - for precision in ['fast', 'safe', 'exact']: - _empty_alpha(precision) - _one_3d_point_alpha(precision) - -def _infinite_alpha(precision): - point_list = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]] - alpha_complex = AlphaComplex3D(points=point_list, precision = precision) - assert alpha_complex.__is_defined() == True - - stree = alpha_complex.create_simplex_tree() - assert stree.__is_persistence_defined() == False - - assert stree.num_simplices() == 51 - assert stree.num_vertices() == len(point_list) - - for filtration in stree.get_filtration(): - if len(filtration[0]) == 1: - assert filtration[1] == 0. - if len(filtration[0]) == 4: - assert filtration[1] == 0.75 - - for idx in range(len(point_list)): - pt_idx = point_list.index(alpha_complex.get_point(idx)) - assert pt_idx >= 0 - assert pt_idx < len(point_list) - - with pytest.raises(IndexError): - alpha_complex.get_point(len(point_list)) - -def test_infinite_alpha(): - for precision in ['fast', 'safe', 'exact']: - _infinite_alpha(precision) - -def _filtered_alpha(precision): - point_list = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]] - filtered_alpha = AlphaComplex3D(points=point_list, precision = precision) - - stree = filtered_alpha.create_simplex_tree(max_alpha_square=0.25) - - assert stree.num_simplices() == 20 - assert stree.num_vertices() == len(point_list) - - for filtration in stree.get_filtration(): - if len(filtration[0]) == 1: - assert filtration[1] == 0. - elif len(filtration[0]) == 2: - assert filtration[1] == 0.25 - else: - assert False - - for idx in range(len(point_list)): - pt_idx = point_list.index(filtered_alpha.get_point(idx)) - assert pt_idx >= 0 - assert pt_idx < len(point_list) - - with pytest.raises(IndexError): - filtered_alpha.get_point(len(point_list)) - -def test_filtered_alpha(): - for precision in ['fast', 'safe', 'exact']: - _filtered_alpha(precision) - -def _3d_points_on_a_plane(precision): - alpha = AlphaComplex3D(points = [[1.0, 1.0 , 0.0], - [7.0, 0.0 , 0.0], - [4.0, 6.0 , 0.0], - [9.0, 6.0 , 0.0], - [0.0, 14.0, 0.0], - [2.0, 19.0, 0.0], - [9.0, 17.0, 0.0]], precision = precision) - - with pytest.raises(ValueError): - stree = alpha.create_simplex_tree() - -def test_3d_points_on_a_plane(): - for precision in ['fast', 'safe', 'exact']: - _3d_points_on_a_plane(precision) - -def test_inconsistency_points_and_weights(): - points = [[1.0, 1.0 , 1.0], - [7.0, 0.0 , 2.0], - [4.0, 6.0 , 0.0], - [9.0, 6.0 , 1.0], - [0.0, 14.0, 2.0], - [2.0, 19.0, 0.0], - [9.0, 17.0, 1.0]] - with pytest.raises(ValueError): - # 7 points, 8 weights, on purpose - alpha = AlphaComplex3D(points = points, - weights = [1., 2., 3., 4., 5., 6., 7., 8.]) - - with pytest.raises(ValueError): - # 7 points, 6 weights, on purpose - alpha = AlphaComplex3D(points = points, - weights = [1., 2., 3., 4., 5., 6.]) - -def _weighted_doc_example(precision): - pts = [[ 1., -1., -1.], - [-1., 1., -1.], - [-1., -1., 1.], - [ 1., 1., 1.], - [ 2., 2., 2.]] - wgts = [4., 4., 4., 4., 1.] - alpha = AlphaComplex3D(points = pts, weights = wgts, precision = precision) - stree = alpha.create_simplex_tree() - - # Needs to retrieve points as points are shuffled - get_idx = lambda idx: pts.index(alpha.get_point(idx)) - indices = [get_idx(x) for x in range(len(pts))] - - assert stree.filtration([indices[x] for x in [0, 1, 2, 3]]) == pytest.approx(-1.) - assert stree.filtration([indices[x] for x in [0, 1, 3, 4]]) == pytest.approx(95.) - assert stree.filtration([indices[x] for x in [0, 2, 3, 4]]) == pytest.approx(95.) - assert stree.filtration([indices[x] for x in [1, 2, 3, 4]]) == pytest.approx(95.) - -def test_weighted_doc_example(): - for precision in ['fast', 'safe', 'exact']: - _weighted_doc_example(precision) - -def test_points_not_in_3d(): - with pytest.raises(ValueError): - alpha = AlphaComplex3D(points = np.random.rand(6,2)) - with pytest.raises(ValueError): - alpha = AlphaComplex3D(points = np.random.rand(6,4)) - - alpha = AlphaComplex3D(points = np.random.rand(6,3)) - assert alpha.__is_defined() == True \ No newline at end of file -- cgit v1.2.3 From aa1a040f6338ec938b9360de35196ad0518be502 Mon Sep 17 00:00:00 2001 From: Hind-M 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/gudhi') 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/gudhi') 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 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/gudhi') 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 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/gudhi') 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/gudhi') 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 0220788ba1710712c275fa8a0e1d16e5279fa266 Mon Sep 17 00:00:00 2001 From: VincentRouvreau Date: Mon, 6 Sep 2021 15:47:05 +0200 Subject: code review: weights is None by default, even for documentation --- src/python/gudhi/alpha_complex.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index 2cf4738b..446f4123 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -53,7 +53,7 @@ cdef class AlphaComplex: cdef Alpha_complex_interface * this_ptr # Fake constructor that does nothing but documenting the constructor - def __init__(self, points=[], off_file='', weights=[], precision='safe'): + def __init__(self, points=[], off_file='', weights=None, precision='safe'): """AlphaComplex constructor. :param points: A list of points in d-Dimension. -- 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/gudhi') 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 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/gudhi') 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/gudhi') 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/gudhi') 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 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/gudhi') 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 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/gudhi') 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 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/gudhi') 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 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/gudhi') 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/gudhi') 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/gudhi') 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 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/gudhi') 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 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/gudhi') 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/gudhi') 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/gudhi') 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/gudhi') 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 821f616cc35ac890a90aba43e1f8124201e5c4f3 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 8 Nov 2021 09:22:17 +0100 Subject: code review: read_points_from_off_file already throws an exception when off file does not exist --- src/python/gudhi/alpha_complex.pyx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index 446f4123..a4888914 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -16,8 +16,6 @@ from libcpp.utility cimport pair from libcpp.string cimport string from libcpp cimport bool from libc.stdint cimport intptr_t -import errno -import os import warnings from gudhi.simplex_tree cimport * @@ -82,10 +80,7 @@ cdef class AlphaComplex: if off_file: warnings.warn("off_file is a deprecated parameter, please consider using gudhi.read_points_from_off_file", DeprecationWarning) - if os.path.isfile(off_file): - points = read_points_from_off_file(off_file = off_file) - else: - raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), off_file) + points = read_points_from_off_file(off_file = off_file) # weights are set but is inconsistent with the number of points if weights != None and len(weights) != len(points): -- 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/gudhi') 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 27d66e5a8a101d80a7dd8b1f21e1cdfb7dedd98e Mon Sep 17 00:00:00 2001 From: Hind-M Date: Wed, 24 Nov 2021 11:03:18 +0100 Subject: Make the new BettiCurve class compatible with the old interface --- src/python/CMakeLists.txt | 4 +- src/python/gudhi/representations/vector_methods.py | 128 ++++++++++----------- .../test/test_betti_curve_representations.py | 15 ++- 3 files changed, 74 insertions(+), 73 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 26b8b7d6..2a5b961b 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -535,8 +535,8 @@ if(PYTHONINTERP_FOUND) add_gudhi_py_test(test_representations) endif() - # Betti curves. - if(SCIPY_FOUND) + # Betti curves + if(SKLEARN_FOUND AND SCIPY_FOUND) add_gudhi_py_test(test_betti_curve_representations) endif() diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 018e9b21..f1232040 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -311,12 +311,14 @@ class Silhouette(BaseEstimator, TransformerMixin): class BettiCurve(BaseEstimator, TransformerMixin): """ - Compute Betti curves from persistence diagrams. There are two modes of operation: with a predefined grid, and without. With a predefined grid, the class computes the Betti numbers at those grid points. Without a predefined grid, it can be fit to a list of persistence diagrams and produce a grid that consists of (at least) the filtration values at which at least one of those persistence diagrams changes Betti numbers, and then compute the Betti numbers at those grid points. In the latter mode, the exact Betti curve is computed for the entire real line. + Compute Betti curves from persistence diagrams. There are several modes of operation: with a given resolution (with or without a sample_range), with a predefined grid, and with none of the previous. With a predefined grid, the class computes the Betti numbers at those grid points. Without a predefined grid, if the resolution is set to None, it can be fit to a list of persistence diagrams and produce a grid that consists of (at least) the filtration values at which at least one of those persistence diagrams changes Betti numbers, and then compute the Betti numbers at those grid points. In the latter mode, the exact Betti curve is computed for the entire real line. Otherwise, if the resolution is given, the Betti curve is obtained by sampling evenly using either the given sample_range or based on the persistence diagrams. Parameters ---------- - predefined_grid: 1d array, triple or None, default=None - Predefined filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinities are OK. If a triple of the form (l, u, n), the grid will be uniform from l to u in n steps. If None (default), a grid will be computed that captures all changes in Betti numbers in the provided data. + resolution (int): number of sample for the piecewise-constant function (default 100). + sample_range ([double, double]): minimum and maximum of the piecewise-constant function domain, of the form [x_min, x_max] (default [numpy.nan, numpy.nan]). It is the interval on which samples will be drawn evenly. If one of the values is numpy.nan, it can be computed from the persistence diagrams with the fit() method. + predefined_grid: 1d array or None, default=None + Predefined filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinities are OK. If None (default), and resolution is given, the grid will be uniform from x_min to x_max in 'resolution' steps, otherwise a grid will be computed that captures all changes in Betti numbers in the provided data. Attributes ---------- @@ -326,34 +328,31 @@ class BettiCurve(BaseEstimator, TransformerMixin): Examples -------- If pd is a persistence diagram and xs is a nonempty grid of finite values such that xs[0] >= pd.min(), then the result of - >>> bc = BettiCurve(xs) + >>> bc = BettiCurve(predefined_grid=xs) >>> result = bc(pd) and >>> from scipy.interpolate import interp1d - >>> bc = BettiCurve(None) + >>> bc = BettiCurve(resolution=None, predefined_grid=None) >>> bettis = bc.fit_transform([pd]) >>> interp = interp1d(bc.grid_, bettis[0, :], kind="previous", fill_value="extrapolate") >>> result = np.array(interp(xs), dtype=int) are the same. """ - def __init__(self, predefined_grid = None): - if isinstance(predefined_grid, tuple): - if len(predefined_grid) != 3: - raise ValueError("Expected array, None or triple.") + def __init__(self, resolution=100, sample_range=[np.nan, np.nan], predefined_grid=None): + if (predefined_grid is not None) and (not isinstance(predefined_grid, np.ndarray)): + raise ValueError("Expected array or None.") - self.predefined_grid = np.linspace(predefined_grid[0], predefined_grid[1], predefined_grid[2]) - else: - self.predefined_grid = predefined_grid + self.predefined_grid = predefined_grid + self.resolution = resolution + self.sample_range = sample_range - def is_fitted(self): return hasattr(self, "grid_") - def fit(self, X, y = None): """ - Compute a filtration grid that captures all changes in Betti numbers for all the given persistence diagrams, unless a predefined grid was provided. + Fit the BettiCurve class on a list of persistence diagrams: if any of the values in **sample_range** is numpy.nan, replace it with the corresponding value computed on the given list of persistence diagrams. When no predefined grid is provided and resolution set to None, compute a filtration grid that captures all changes in Betti numbers for all the given persistence diagrams. Parameters ---------- @@ -365,60 +364,17 @@ class BettiCurve(BaseEstimator, TransformerMixin): """ if self.predefined_grid is None: - events = np.unique(np.concatenate([pd.flatten() for pd in X] + [[-np.inf]], axis=0)) - self.grid_ = np.array(events) + if self.resolution is None: # Flexible/exact version + events = np.unique(np.concatenate([pd.flatten() for pd in X] + [[-np.inf]], axis=0)) + self.grid_ = np.array(events) + else: + self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) + self.grid_ = np.linspace(self.sample_range[0], self.sample_range[1], self.resolution) else: - self.grid_ = np.array(self.predefined_grid) - - - #self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) + self.grid_ = self.predefined_grid # Get the predefined grid from user return self - - def fit_transform(self, X): - """ - Find a sampling grid that captures all changes in Betti numbers, and compute those Betti numbers. The result is the same as fit(X) followed by transform(X), but potentially faster. - """ - - if self.predefined_grid is None: - if not X: - X = [np.zeros((0, 2))] - - N = len(X) - - events = np.concatenate([pd.flatten(order="F") for pd in X], axis=0) - sorting = np.argsort(events) - offsets = np.zeros(1 + N, dtype=int) - for i in range(0, N): - offsets[i+1] = offsets[i] + 2*X[i].shape[0] - starts = offsets[0:N] - ends = offsets[1:N + 1] - 1 - - xs = [-np.inf] - bettis = [[0] for i in range(0, N)] - - for i in sorting: - j = np.searchsorted(ends, i) - delta = 1 if i - starts[j] < len(X[j]) else -1 - if events[i] == xs[-1]: - bettis[j][-1] += delta - else: - xs.append(events[i]) - for k in range(0, j): - bettis[k].append(bettis[k][-1]) - bettis[j].append(bettis[j][-1] + delta) - for k in range(j+1, N): - bettis[k].append(bettis[k][-1]) - - self.grid_ = np.array(xs) - return np.array(bettis, dtype=int) - - else: - self.grid_ = self.predefined_grid - return self.transform(X) - - def transform(self, X): """ Compute Betti curves. @@ -464,12 +420,52 @@ class BettiCurve(BaseEstimator, TransformerMixin): return np.array(bettis, dtype=int)[:, 0:-1] + def fit_transform(self, X): + """ + Find a sampling grid that captures all changes in Betti numbers, and compute those Betti numbers. The result is the same as fit(X) followed by transform(X), but potentially faster. + """ + + if self.predefined_grid is None and self.resolution is None: + if not X: + X = [np.zeros((0, 2))] + + N = len(X) + + events = np.concatenate([pd.flatten(order="F") for pd in X], axis=0) + sorting = np.argsort(events) + offsets = np.zeros(1 + N, dtype=int) + for i in range(0, N): + offsets[i+1] = offsets[i] + 2*X[i].shape[0] + starts = offsets[0:N] + ends = offsets[1:N + 1] - 1 + + xs = [-np.inf] + bettis = [[0] for i in range(0, N)] + + for i in sorting: + j = np.searchsorted(ends, i) + delta = 1 if i - starts[j] < len(X[j]) else -1 + if events[i] == xs[-1]: + bettis[j][-1] += delta + else: + xs.append(events[i]) + for k in range(0, j): + bettis[k].append(bettis[k][-1]) + bettis[j].append(bettis[j][-1] + delta) + for k in range(j+1, N): + bettis[k].append(bettis[k][-1]) + + self.grid_ = np.array(xs) + return np.array(bettis, dtype=int) + + else: + return self.fit(X).transform(X) def __call__(self, diag): """ Shorthand for transform on a single persistence diagram. """ - return self.transform([diag])[0, :] + return self.fit_transform([diag])[0, :] diff --git a/src/python/test/test_betti_curve_representations.py b/src/python/test/test_betti_curve_representations.py index 3e77d760..6a45da4d 100755 --- a/src/python/test/test_betti_curve_representations.py +++ b/src/python/test/test_betti_curve_representations.py @@ -1,5 +1,6 @@ import numpy as np import scipy.interpolate +import pytest from gudhi.representations.vector_methods import BettiCurve @@ -19,18 +20,18 @@ def test_betti_curve_is_irregular_betti_curve_followed_by_interpolation(): pd[np.random.uniform(0, 1, n) < pinf, 1] = np.inf pds.append(pd) - bc = BettiCurve(None) + bc = BettiCurve(resolution=None, predefined_grid=None) bc.fit(pds) bettis = bc.transform(pds) - bc2 = BettiCurve(None) + bc2 = BettiCurve(resolution=None, predefined_grid=None) bettis2 = bc2.fit_transform(pds) assert((bc2.grid_ == bc.grid_).all()) assert((bettis2 == bettis).all()) for i in range(0, m): grid = np.linspace(pds[i][np.isfinite(pds[i])].min(), pds[i][np.isfinite(pds[i])].max() + 1, res) - bc_gridded = BettiCurve(grid) + bc_gridded = BettiCurve(predefined_grid=grid) bc_gridded.fit([]) bettis_gridded = bc_gridded(pds[i]) @@ -41,14 +42,18 @@ def test_betti_curve_is_irregular_betti_curve_followed_by_interpolation(): def test_empty_with_predefined_grid(): random_grid = np.sort(np.random.uniform(0, 1, 100)) - bc = BettiCurve(random_grid) + bc = BettiCurve(predefined_grid=random_grid) bettis = bc.fit_transform([]) assert((bc.grid_ == random_grid).all()) assert((bettis == 0).all()) def test_empty(): - bc = BettiCurve() + bc = BettiCurve(resolution=None, predefined_grid=None) bettis = bc.fit_transform([]) assert(bc.grid_ == [-np.inf]) assert((bettis == 0).all()) + +def test_wrong_value_of_predefined_grid(): + with pytest.raises(ValueError): + BettiCurve(predefined_grid=[1, 2, 3]) -- 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/gudhi') 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 d4303ede6ee862141e7fc89811d0d69b0b90a107 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Tue, 18 Jan 2022 10:57:56 +0100 Subject: Fix BettiCurve doc in source code --- src/python/gudhi/representations/vector_methods.py | 78 ++++++++++------------ 1 file changed, 37 insertions(+), 41 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index f1232040..f8078d03 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -312,36 +312,40 @@ class Silhouette(BaseEstimator, TransformerMixin): class BettiCurve(BaseEstimator, TransformerMixin): """ Compute Betti curves from persistence diagrams. There are several modes of operation: with a given resolution (with or without a sample_range), with a predefined grid, and with none of the previous. With a predefined grid, the class computes the Betti numbers at those grid points. Without a predefined grid, if the resolution is set to None, it can be fit to a list of persistence diagrams and produce a grid that consists of (at least) the filtration values at which at least one of those persistence diagrams changes Betti numbers, and then compute the Betti numbers at those grid points. In the latter mode, the exact Betti curve is computed for the entire real line. Otherwise, if the resolution is given, the Betti curve is obtained by sampling evenly using either the given sample_range or based on the persistence diagrams. + """ - Parameters - ---------- - resolution (int): number of sample for the piecewise-constant function (default 100). - sample_range ([double, double]): minimum and maximum of the piecewise-constant function domain, of the form [x_min, x_max] (default [numpy.nan, numpy.nan]). It is the interval on which samples will be drawn evenly. If one of the values is numpy.nan, it can be computed from the persistence diagrams with the fit() method. - predefined_grid: 1d array or None, default=None - Predefined filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinities are OK. If None (default), and resolution is given, the grid will be uniform from x_min to x_max in 'resolution' steps, otherwise a grid will be computed that captures all changes in Betti numbers in the provided data. + def __init__(self, resolution=100, sample_range=[np.nan, np.nan], predefined_grid=None): + """ + Constructor for the BettiCurve class. - Attributes - ---------- - grid_: 1d array - The grid on which the Betti numbers are computed. If predefined_grid was specified, grid_ will always be that grid, independently of data. If not, the grid is fitted to capture all filtration values at which the Betti numbers change. + Parameters: + resolution (int): number of sample for the piecewise-constant function (default 100). + sample_range ([double, double]): minimum and maximum of the piecewise-constant function domain, of the form [x_min, x_max] (default [numpy.nan, numpy.nan]). It is the interval on which samples will be drawn evenly. If one of the values is numpy.nan, it can be computed from the persistence diagrams with the fit() method. + predefined_grid (1d array or None, default=None): Predefined filtration grid points at which to compute the Betti curves. Must be strictly ordered. Infinities are ok. If None (default), and resolution is given, the grid will be uniform from x_min to x_max in 'resolution' steps, otherwise a grid will be computed that captures all changes in Betti numbers in the provided data. - Examples - -------- - If pd is a persistence diagram and xs is a nonempty grid of finite values such that xs[0] >= pd.min(), then the result of - >>> bc = BettiCurve(predefined_grid=xs) - >>> result = bc(pd) - and - >>> from scipy.interpolate import interp1d - >>> bc = BettiCurve(resolution=None, predefined_grid=None) - >>> bettis = bc.fit_transform([pd]) - >>> interp = interp1d(bc.grid_, bettis[0, :], kind="previous", fill_value="extrapolate") - >>> result = np.array(interp(xs), dtype=int) - are the same. - """ + Attributes: + grid_ (1d array): The grid on which the Betti numbers are computed. If predefined_grid was specified, `grid_` will always be that grid, independently of data. If not, the grid is fitted to capture all filtration values at which the Betti numbers change. + + Examples + -------- + If pd is a persistence diagram and xs is a nonempty grid of finite values such that xs[0] >= pd.min(), then the results of: + + >>> bc = BettiCurve(predefined_grid=xs) # doctest: +SKIP + >>> result = bc(pd) # doctest: +SKIP + + and + + >>> from scipy.interpolate import interp1d # doctest: +SKIP + >>> bc = BettiCurve(resolution=None, predefined_grid=None) # doctest: +SKIP + >>> bettis = bc.fit_transform([pd]) # doctest: +SKIP + >>> interp = interp1d(bc.grid_, bettis[0, :], kind="previous", fill_value="extrapolate") # doctest: +SKIP + >>> result = np.array(interp(xs), dtype=int) # doctest: +SKIP + + are the same. + """ - def __init__(self, resolution=100, sample_range=[np.nan, np.nan], predefined_grid=None): if (predefined_grid is not None) and (not isinstance(predefined_grid, np.ndarray)): - raise ValueError("Expected array or None.") + raise ValueError("Expected predefined_grid as array or None.") self.predefined_grid = predefined_grid self.resolution = resolution @@ -354,13 +358,9 @@ class BettiCurve(BaseEstimator, TransformerMixin): """ Fit the BettiCurve class on a list of persistence diagrams: if any of the values in **sample_range** is numpy.nan, replace it with the corresponding value computed on the given list of persistence diagrams. When no predefined grid is provided and resolution set to None, compute a filtration grid that captures all changes in Betti numbers for all the given persistence diagrams. - Parameters - ---------- - X: list of 2d arrays - Persistence diagrams. - - y: None. - Ignored. + Parameters: + X (list of 2d arrays): Persistence diagrams. + y (None): Ignored. """ if self.predefined_grid is None: @@ -379,15 +379,11 @@ class BettiCurve(BaseEstimator, TransformerMixin): """ Compute Betti curves. - Parameters - ---------- - X: list of 2d arrays - Persistence diagrams. + Parameters: + X (list of 2d arrays): Persistence diagrams. - Returns - ------- - (len(X))x(len(self.grid_)) array of ints - Betti numbers of the given persistence diagrams at the grid points given in self.grid_. + Returns: + `len(X).len(self.grid_)` array of ints: Betti numbers of the given persistence diagrams at the grid points given in `self.grid_` """ if not self.is_fitted(): @@ -422,7 +418,7 @@ class BettiCurve(BaseEstimator, TransformerMixin): def fit_transform(self, X): """ - Find a sampling grid that captures all changes in Betti numbers, and compute those Betti numbers. The result is the same as fit(X) followed by transform(X), but potentially faster. + The result is the same as fit(X) followed by transform(X), but potentially faster. """ if self.predefined_grid is None and self.resolution is None: -- cgit v1.2.3 From c1cf7fe36a65b6a97739f673ee55c77e43807746 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Wed, 19 Jan 2022 09:26:00 +0100 Subject: Code review: None was used to detect unweighted alpha complex. An empty list works as well and simplifies the code --- src/python/gudhi/alpha_complex.pyx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index a4888914..4fbbb3ba 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -72,7 +72,7 @@ cdef class AlphaComplex: """ # The real cython constructor - def __cinit__(self, points = [], off_file = '', weights=None, precision = 'safe'): + def __cinit__(self, points = [], off_file = '', weights=[], precision = 'safe'): assert precision in ['fast', 'safe', 'exact'], "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' @@ -83,15 +83,14 @@ cdef class AlphaComplex: points = read_points_from_off_file(off_file = off_file) # weights are set but is inconsistent with the number of points - if weights != None and len(weights) != len(points): + if len(weights) != 0 and len(weights) != len(points): raise ValueError("Inconsistency between the number of points and weights") # need to copy the points to use them without the gil cdef vector[vector[double]] pts cdef vector[double] wgts pts = points - if weights != None: - wgts = weights + wgts = weights with nogil: self.this_ptr = new Alpha_complex_interface(pts, wgts, fast, exact) -- cgit v1.2.3 From 5c09f30e85aa8c50f686265c12987945f0e0a618 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Wed, 19 Jan 2022 09:28:37 +0100 Subject: Code review: update doc as well for c1cf7fe3 --- src/python/gudhi/alpha_complex.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index 4fbbb3ba..512597b3 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -51,7 +51,7 @@ cdef class AlphaComplex: cdef Alpha_complex_interface * this_ptr # Fake constructor that does nothing but documenting the constructor - def __init__(self, points=[], off_file='', weights=None, precision='safe'): + def __init__(self, points=[], off_file='', weights=[], precision='safe'): """AlphaComplex constructor. :param points: A list of points in d-Dimension. -- cgit v1.2.3 From cee14f45b10d8fd4bee78b4323dea650f8d20f11 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Thu, 20 Jan 2022 17:26:27 +0100 Subject: Rollback c1cf7fe and 5c09f30 --- src/python/gudhi/alpha_complex.pyx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/alpha_complex.pyx b/src/python/gudhi/alpha_complex.pyx index 512597b3..a4888914 100644 --- a/src/python/gudhi/alpha_complex.pyx +++ b/src/python/gudhi/alpha_complex.pyx @@ -51,7 +51,7 @@ cdef class AlphaComplex: cdef Alpha_complex_interface * this_ptr # Fake constructor that does nothing but documenting the constructor - def __init__(self, points=[], off_file='', weights=[], precision='safe'): + def __init__(self, points=[], off_file='', weights=None, precision='safe'): """AlphaComplex constructor. :param points: A list of points in d-Dimension. @@ -72,7 +72,7 @@ cdef class AlphaComplex: """ # The real cython constructor - def __cinit__(self, points = [], off_file = '', weights=[], precision = 'safe'): + def __cinit__(self, points = [], off_file = '', weights=None, precision = 'safe'): assert precision in ['fast', 'safe', 'exact'], "Alpha complex precision can only be 'fast', 'safe' or 'exact'" cdef bool fast = precision == 'fast' cdef bool exact = precision == 'exact' @@ -83,14 +83,15 @@ cdef class AlphaComplex: points = read_points_from_off_file(off_file = off_file) # weights are set but is inconsistent with the number of points - if len(weights) != 0 and len(weights) != len(points): + if weights != None and len(weights) != len(points): raise ValueError("Inconsistency between the number of points and weights") # need to copy the points to use them without the gil cdef vector[vector[double]] pts cdef vector[double] wgts pts = points - wgts = weights + if weights != None: + wgts = weights with nogil: self.this_ptr = new Alpha_complex_interface(pts, wgts, fast, exact) -- cgit v1.2.3 From 37a141533397568e7070c734e21ef9c4dc85d132 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Thu, 10 Feb 2022 16:16:14 +0100 Subject: Add SimplexTree copy method and its test --- src/python/gudhi/simplex_tree.pxd | 1 + src/python/gudhi/simplex_tree.pyx | 14 ++++++++++++++ src/python/test/test_simplex_tree.py | 19 ++++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index 006a24ed..92139db4 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -45,6 +45,7 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": cdef cppclass Simplex_tree_interface_full_featured "Gudhi::Simplex_tree_interface": Simplex_tree_interface_full_featured() nogil + Simplex_tree_interface_full_featured(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 diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index c3720936..6b3116a4 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -63,6 +63,20 @@ cdef class SimplexTree: """ return self.pcohptr != NULL + def copy(self): + """ + :returns: A simplex tree that is a deep copy itself. + :rtype: SimplexTree + """ + stree = SimplexTree() + cdef Simplex_tree_interface_full_featured* stree_ptr + cdef Simplex_tree_interface_full_featured* self_ptr=self.get_ptr() + with nogil: + stree_ptr = new Simplex_tree_interface_full_featured(dereference(self_ptr)) + + stree.thisptr = (stree_ptr) + return stree + def filtration(self, simplex): """This function returns the filtration value for a given N-simplex in this simplicial complex, or +infinity if it is not in the complex. diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index 31c46213..dac45288 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -447,4 +447,21 @@ def test_persistence_intervals_in_dimension(): 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 + +def test_simplex_tree_copy(): + st = SimplexTree() + st .insert([1,2,3], 0.) + a = st.copy() + # TODO(VR): when #463 is merged, replace with + # assert a == st + assert a.num_vertices() == st.num_vertices() + assert a.num_simplices() == st.num_simplices() + st_filt_list = list(st.get_filtration()) + assert list(a.get_filtration()) == st_filt_list + + a.remove_maximal_simplex([1, 2, 3]) + a_filt_list = list(a.get_filtration()) + assert len(a_filt_list) < len(st_filt_list) + + for a_splx in a_filt_list: + assert a_splx in st_filt_list -- cgit v1.2.3 From fb8ce008feadcaf6a936740a3ed54d50970c731c Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Fri, 11 Feb 2022 23:11:26 +0100 Subject: __copy__, __deepcopy__, copy, and copy ctors. Still pb with the doc --- .github/next_release.md | 3 ++ src/python/gudhi/simplex_tree.pyx | 54 +++++++++++++++---- src/python/test/test_simplex_tree.py | 102 +++++++++++++++++++++++++++++++---- 3 files changed, 140 insertions(+), 19 deletions(-) (limited to 'src/python/gudhi') diff --git a/.github/next_release.md b/.github/next_release.md index e21b25c7..3946404b 100644 --- a/.github/next_release.md +++ b/.github/next_release.md @@ -13,6 +13,9 @@ Below is a list of changes made since GUDHI 3.5.0: - [Representations](https://gudhi.inria.fr/python/latest/representations.html#gudhi.representations.vector_methods.BettiCurve) - A more flexible Betti curve class capable of computing exact curves +- [Simplex tree](https://gudhi.inria.fr/python/latest/simplex_tree_ref.html) + - `__copy__`, `__deepcopy__`, `copy` and copy constructors + - Installation - Boost ≥ 1.66.0 is now required (was ≥ 1.56.0). diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 6b3116a4..ed7c3b92 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -30,6 +30,7 @@ cdef class SimplexTree: # unfortunately 'cdef public Simplex_tree_interface_full_featured* thisptr' is not possible # Use intptr_t instead to cast the pointer cdef public intptr_t thisptr + cdef bool __thisptr_to_be_deleted # Get the pointer casted as it should be cdef Simplex_tree_interface_full_featured* get_ptr(self) nogil: @@ -38,17 +39,36 @@ cdef class SimplexTree: cdef Simplex_tree_persistence_interface * pcohptr # Fake constructor that does nothing but documenting the constructor - def __init__(self): + def __init__(self, other = None, copy = True): """SimplexTree constructor. + :param other: If `other` is a SimplexTree (default = None), the SimplexTree is constructed from a deep/shallow copy of `other`. + :type other: SimplexTree + :param copy: If `True`, the copy will be deep and if `False, the copy will be shallow. Default is `True`. + :type copy: bool + :returns: A simplex tree that is a (deep or shallow) copy of itself. + :rtype: SimplexTree + :note: copy constructor requires :func:`compute_persistence` to be launched again as the result is not copied. """ # The real cython constructor - def __cinit__(self): - self.thisptr = (new Simplex_tree_interface_full_featured()) + def __cinit__(self, other = None, copy = True): + cdef SimplexTree ostr + if other and type(other) is SimplexTree: + ostr = other + if copy: + self.thisptr = (new Simplex_tree_interface_full_featured(dereference(ostr.get_ptr()))) + else: + self.thisptr = ostr.thisptr + # Avoid double free - The original is in charge of deletion + self.__thisptr_to_be_deleted = False + else: + self.__thisptr_to_be_deleted = True + self.thisptr = (new Simplex_tree_interface_full_featured()) def __dealloc__(self): cdef Simplex_tree_interface_full_featured* ptr = self.get_ptr() - if ptr != NULL: + # Avoid double free - The original is in charge of deletion + if ptr != NULL and self.__thisptr_to_be_deleted: del ptr if self.pcohptr != NULL: del self.pcohptr @@ -63,20 +83,34 @@ cdef class SimplexTree: """ return self.pcohptr != NULL - def copy(self): + def copy(self, deep=True): """ - :returns: A simplex tree that is a deep copy itself. + :param deep: If `True`, the copy will be deep and if `False`, the copy will be shallow. Default is `True`. + :type deep: bool + :returns: A simplex tree that is a (deep or shallow) copy of itself. :rtype: SimplexTree + :note: copy requires :func:`compute_persistence` to be launched again as the result is not copied. """ stree = SimplexTree() cdef Simplex_tree_interface_full_featured* stree_ptr cdef Simplex_tree_interface_full_featured* self_ptr=self.get_ptr() - with nogil: - stree_ptr = new Simplex_tree_interface_full_featured(dereference(self_ptr)) - - stree.thisptr = (stree_ptr) + if deep: + with nogil: + stree_ptr = new Simplex_tree_interface_full_featured(dereference(self_ptr)) + + stree.thisptr = (stree_ptr) + else: + stree.thisptr = self.thisptr + # Avoid double free - The original is in charge of deletion + stree.__thisptr_to_be_deleted = False return stree + def __copy__(self): + return self.copy(deep=False) + + def __deepcopy__(self): + return self.copy(deep=True) + def filtration(self, simplex): """This function returns the filtration value for a given N-simplex in this simplicial complex, or +infinity if it is not in the complex. diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index dac45288..6db6d8fb 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -448,20 +448,104 @@ def test_persistence_intervals_in_dimension(): # Test empty case assert st.persistence_intervals_in_dimension(3).shape == (0, 2) -def test_simplex_tree_copy(): +def test_simplex_tree_deep_copy(): st = SimplexTree() - st .insert([1,2,3], 0.) - a = st.copy() + st.insert([1, 2, 3], 0.) + # persistence is not copied + st.compute_persistence() + + st_copy = st.copy(deep=True) # TODO(VR): when #463 is merged, replace with - # assert a == st - assert a.num_vertices() == st.num_vertices() - assert a.num_simplices() == st.num_simplices() + # assert st_copy == st + assert st_copy.num_vertices() == st.num_vertices() + assert st_copy.num_simplices() == st.num_simplices() st_filt_list = list(st.get_filtration()) - assert list(a.get_filtration()) == st_filt_list + assert list(st_copy.get_filtration()) == st_filt_list + + assert st.__is_persistence_defined() == True + assert st_copy.__is_persistence_defined() == False - a.remove_maximal_simplex([1, 2, 3]) - a_filt_list = list(a.get_filtration()) + st_copy.remove_maximal_simplex([1, 2, 3]) + a_filt_list = list(st_copy.get_filtration()) assert len(a_filt_list) < len(st_filt_list) for a_splx in a_filt_list: assert a_splx in st_filt_list + + # test double free + del st + del st_copy + +def test_simplex_tree_shallow_copy(): + st = SimplexTree() + st.insert([1, 2, 3], 0.) + # persistence is not copied + st.compute_persistence() + + st_copy = st.copy(deep=False) + # TODO(VR): when #463 is merged, replace with + # assert st_copy == st + assert st_copy.num_vertices() == st.num_vertices() + assert st_copy.num_simplices() == st.num_simplices() + assert list(st_copy.get_filtration()) == list(st.get_filtration()) + + assert st.__is_persistence_defined() == True + assert st_copy.__is_persistence_defined() == False + + st_copy.assign_filtration([1, 2, 3], 2.) + assert list(st_copy.get_filtration()) == list(st.get_filtration()) + + # test double free + del st + del st_copy + +def test_simplex_tree_deep_copy_constructor(): + st = SimplexTree() + st.insert([1, 2, 3], 0.) + # persistence is not copied + st.compute_persistence() + + st_copy = SimplexTree(st, copy = True) + # TODO(VR): when #463 is merged, replace with + # assert st_copy == st + assert st_copy.num_vertices() == st.num_vertices() + assert st_copy.num_simplices() == st.num_simplices() + st_filt_list = list(st.get_filtration()) + assert list(st_copy.get_filtration()) == st_filt_list + + assert st.__is_persistence_defined() == True + assert st_copy.__is_persistence_defined() == False + + st_copy.remove_maximal_simplex([1, 2, 3]) + a_filt_list = list(st_copy.get_filtration()) + assert len(a_filt_list) < len(st_filt_list) + + for a_splx in a_filt_list: + assert a_splx in st_filt_list + + # test double free + del st + del st_copy + +def test_simplex_tree_shallow_copy(): + st = SimplexTree() + st.insert([1, 2, 3], 0.) + # persistence is not copied + st.compute_persistence() + + st_copy = SimplexTree(st, copy = False) + # TODO(VR): when #463 is merged, replace with + # assert st_copy == st + assert st_copy.num_vertices() == st.num_vertices() + assert st_copy.num_simplices() == st.num_simplices() + assert list(st_copy.get_filtration()) == list(st.get_filtration()) + + assert st.__is_persistence_defined() == True + assert st_copy.__is_persistence_defined() == False + + st_copy.assign_filtration([1, 2, 3], 2.) + assert list(st_copy.get_filtration()) == list(st.get_filtration()) + + # test double free + del st + del st_copy -- cgit v1.2.3 From 43981a4d487669fe2002337ab62b72dd9e83a64a Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 14 Feb 2022 11:08:00 +0100 Subject: Remove shallow copy --- .github/next_release.md | 2 +- src/python/gudhi/simplex_tree.pyx | 54 +++++++++++++----------------------- src/python/test/test_simplex_tree.py | 50 ++------------------------------- 3 files changed, 23 insertions(+), 83 deletions(-) (limited to 'src/python/gudhi') diff --git a/.github/next_release.md b/.github/next_release.md index 3946404b..3d4761eb 100644 --- a/.github/next_release.md +++ b/.github/next_release.md @@ -14,7 +14,7 @@ Below is a list of changes made since GUDHI 3.5.0: - A more flexible Betti curve class capable of computing exact curves - [Simplex tree](https://gudhi.inria.fr/python/latest/simplex_tree_ref.html) - - `__copy__`, `__deepcopy__`, `copy` and copy constructors + - `__deepcopy__`, `copy` and copy constructors - Installation - Boost ≥ 1.66.0 is now required (was ≥ 1.56.0). diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index ed7c3b92..0213e363 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -30,7 +30,6 @@ cdef class SimplexTree: # unfortunately 'cdef public Simplex_tree_interface_full_featured* thisptr' is not possible # Use intptr_t instead to cast the pointer cdef public intptr_t thisptr - cdef bool __thisptr_to_be_deleted # Get the pointer casted as it should be cdef Simplex_tree_interface_full_featured* get_ptr(self) nogil: @@ -39,36 +38,32 @@ cdef class SimplexTree: cdef Simplex_tree_persistence_interface * pcohptr # Fake constructor that does nothing but documenting the constructor - def __init__(self, other = None, copy = True): + def __init__(self, other = None): """SimplexTree constructor. - :param other: If `other` is a SimplexTree (default = None), the SimplexTree is constructed from a deep/shallow copy of `other`. + + :param other: If `other` is a `None` (default value), an empty `SimplexTree` is created. + If `other` is a `SimplexTree`, the `SimplexTree` is constructed from a deep copy of `other`. :type other: SimplexTree - :param copy: If `True`, the copy will be deep and if `False, the copy will be shallow. Default is `True`. - :type copy: bool - :returns: A simplex tree that is a (deep or shallow) copy of itself. + :returns: An empty or a copy simplex tree. :rtype: SimplexTree - :note: copy constructor requires :func:`compute_persistence` to be launched again as the result is not copied. + + :note: If the `SimplexTree` is a copy, it requires :func:`compute_persistence` to be launched again as the + persistence result is not copied. """ # The real cython constructor - def __cinit__(self, other = None, copy = True): + def __cinit__(self, other = None): cdef SimplexTree ostr if other and type(other) is SimplexTree: ostr = other - if copy: - self.thisptr = (new Simplex_tree_interface_full_featured(dereference(ostr.get_ptr()))) - else: - self.thisptr = ostr.thisptr - # Avoid double free - The original is in charge of deletion - self.__thisptr_to_be_deleted = False + self.thisptr = (new Simplex_tree_interface_full_featured(dereference(ostr.get_ptr()))) else: - self.__thisptr_to_be_deleted = True self.thisptr = (new Simplex_tree_interface_full_featured()) def __dealloc__(self): cdef Simplex_tree_interface_full_featured* ptr = self.get_ptr() # Avoid double free - The original is in charge of deletion - if ptr != NULL and self.__thisptr_to_be_deleted: + if ptr != NULL: del ptr if self.pcohptr != NULL: del self.pcohptr @@ -83,33 +78,24 @@ cdef class SimplexTree: """ return self.pcohptr != NULL - def copy(self, deep=True): + def copy(self): """ - :param deep: If `True`, the copy will be deep and if `False`, the copy will be shallow. Default is `True`. - :type deep: bool - :returns: A simplex tree that is a (deep or shallow) copy of itself. + :returns: A simplex tree that is a deep copy of itself. :rtype: SimplexTree - :note: copy requires :func:`compute_persistence` to be launched again as the result is not copied. + + :note: copy requires :func:`compute_persistence` to be launched again as the persistence result is not copied. """ stree = SimplexTree() cdef Simplex_tree_interface_full_featured* stree_ptr cdef Simplex_tree_interface_full_featured* self_ptr=self.get_ptr() - if deep: - with nogil: - stree_ptr = new Simplex_tree_interface_full_featured(dereference(self_ptr)) - - stree.thisptr = (stree_ptr) - else: - stree.thisptr = self.thisptr - # Avoid double free - The original is in charge of deletion - stree.__thisptr_to_be_deleted = False - return stree + with nogil: + stree_ptr = new Simplex_tree_interface_full_featured(dereference(self_ptr)) - def __copy__(self): - return self.copy(deep=False) + stree.thisptr = (stree_ptr) + return stree def __deepcopy__(self): - return self.copy(deep=True) + return self.copy() def filtration(self, simplex): """This function returns the filtration value for a given N-simplex in diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index 6db6d8fb..62dcc865 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -454,7 +454,7 @@ def test_simplex_tree_deep_copy(): # persistence is not copied st.compute_persistence() - st_copy = st.copy(deep=True) + st_copy = st.copy() # TODO(VR): when #463 is merged, replace with # assert st_copy == st assert st_copy.num_vertices() == st.num_vertices() @@ -476,36 +476,13 @@ def test_simplex_tree_deep_copy(): del st del st_copy -def test_simplex_tree_shallow_copy(): - st = SimplexTree() - st.insert([1, 2, 3], 0.) - # persistence is not copied - st.compute_persistence() - - st_copy = st.copy(deep=False) - # TODO(VR): when #463 is merged, replace with - # assert st_copy == st - assert st_copy.num_vertices() == st.num_vertices() - assert st_copy.num_simplices() == st.num_simplices() - assert list(st_copy.get_filtration()) == list(st.get_filtration()) - - assert st.__is_persistence_defined() == True - assert st_copy.__is_persistence_defined() == False - - st_copy.assign_filtration([1, 2, 3], 2.) - assert list(st_copy.get_filtration()) == list(st.get_filtration()) - - # test double free - del st - del st_copy - def test_simplex_tree_deep_copy_constructor(): st = SimplexTree() st.insert([1, 2, 3], 0.) # persistence is not copied st.compute_persistence() - st_copy = SimplexTree(st, copy = True) + st_copy = SimplexTree(st) # TODO(VR): when #463 is merged, replace with # assert st_copy == st assert st_copy.num_vertices() == st.num_vertices() @@ -526,26 +503,3 @@ def test_simplex_tree_deep_copy_constructor(): # test double free del st del st_copy - -def test_simplex_tree_shallow_copy(): - st = SimplexTree() - st.insert([1, 2, 3], 0.) - # persistence is not copied - st.compute_persistence() - - st_copy = SimplexTree(st, copy = False) - # TODO(VR): when #463 is merged, replace with - # assert st_copy == st - assert st_copy.num_vertices() == st.num_vertices() - assert st_copy.num_simplices() == st.num_simplices() - assert list(st_copy.get_filtration()) == list(st.get_filtration()) - - assert st.__is_persistence_defined() == True - assert st_copy.__is_persistence_defined() == False - - st_copy.assign_filtration([1, 2, 3], 2.) - assert list(st_copy.get_filtration()) == list(st.get_filtration()) - - # test double free - del st - del st_copy -- cgit v1.2.3 From c7303733b28d3bc429d4bdfd030409e07430599d Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 14 Feb 2022 11:29:49 +0100 Subject: Rewrite test as #463 is merged. Rephrase constructor documentation --- src/python/gudhi/simplex_tree.pyx | 4 ++-- src/python/test/test_simplex_tree.py | 20 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 6701d98d..a2b8719a 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -41,9 +41,9 @@ cdef class SimplexTree: def __init__(self, other = None): """SimplexTree constructor. - :param other: If `other` is a `None` (default value), an empty `SimplexTree` is created. + :param other: If `other` is `None` (default value), an empty `SimplexTree` is created. If `other` is a `SimplexTree`, the `SimplexTree` is constructed from a deep copy of `other`. - :type other: SimplexTree + :type other: SimplexTree (Optional) :returns: An empty or a copy simplex tree. :rtype: SimplexTree diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index 32fc63ec..2c2e09a2 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -463,20 +463,18 @@ def test_equality_operator(): def test_simplex_tree_deep_copy(): st = SimplexTree() st.insert([1, 2, 3], 0.) - # persistence is not copied + # compute persistence only on the original st.compute_persistence() st_copy = st.copy() - # TODO(VR): when #463 is merged, replace with - # assert st_copy == st - assert st_copy.num_vertices() == st.num_vertices() - assert st_copy.num_simplices() == st.num_simplices() + assert st_copy == st st_filt_list = list(st.get_filtration()) - assert list(st_copy.get_filtration()) == st_filt_list + # check persistence is not copied assert st.__is_persistence_defined() == True assert st_copy.__is_persistence_defined() == False + # remove something in the copy and check the copy is included in the original st_copy.remove_maximal_simplex([1, 2, 3]) a_filt_list = list(st_copy.get_filtration()) assert len(a_filt_list) < len(st_filt_list) @@ -491,20 +489,18 @@ def test_simplex_tree_deep_copy(): def test_simplex_tree_deep_copy_constructor(): st = SimplexTree() st.insert([1, 2, 3], 0.) - # persistence is not copied + # compute persistence only on the original st.compute_persistence() st_copy = SimplexTree(st) - # TODO(VR): when #463 is merged, replace with - # assert st_copy == st - assert st_copy.num_vertices() == st.num_vertices() - assert st_copy.num_simplices() == st.num_simplices() + assert st_copy == st st_filt_list = list(st.get_filtration()) - assert list(st_copy.get_filtration()) == st_filt_list + # check persistence is not copied assert st.__is_persistence_defined() == True assert st_copy.__is_persistence_defined() == False + # remove something in the copy and check the copy is included in the original st_copy.remove_maximal_simplex([1, 2, 3]) a_filt_list = list(st_copy.get_filtration()) assert len(a_filt_list) < len(st_filt_list) -- cgit v1.2.3 From 1a0551c033e55024cd0f00302cd9df1f356bab44 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 14 Feb 2022 11:31:53 +0100 Subject: some left over --- src/python/gudhi/simplex_tree.pyx | 1 - 1 file changed, 1 deletion(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index a2b8719a..8f760422 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -62,7 +62,6 @@ cdef class SimplexTree: def __dealloc__(self): cdef Simplex_tree_interface_full_featured* ptr = self.get_ptr() - # Avoid double free - The original is in charge of deletion if ptr != NULL: del ptr if self.pcohptr != NULL: -- cgit v1.2.3 From d5ac245a6dc4ab2d6e30689fc5d95503c40b6187 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 14 Feb 2022 12:31:30 +0100 Subject: code review: ctor shall raise a TypeError when constructed from something else than a SimplexTree --- src/python/gudhi/simplex_tree.pyx | 10 +++++++--- src/python/test/test_simplex_tree.py | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 8f760422..e1685ded 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -47,6 +47,7 @@ cdef class SimplexTree: :returns: An empty or a copy simplex tree. :rtype: SimplexTree + :raises TypeError: In case `other` is neither `None`, nor a `SimplexTree`. :note: If the `SimplexTree` is a copy, it requires :func:`compute_persistence` to be launched again as the persistence result is not copied. """ @@ -54,9 +55,12 @@ cdef class SimplexTree: # The real cython constructor def __cinit__(self, other = None): cdef SimplexTree ostr - if other and type(other) is SimplexTree: - ostr = other - self.thisptr = (new Simplex_tree_interface_full_featured(dereference(ostr.get_ptr()))) + if other: + if type(other) is SimplexTree: + ostr = other + self.thisptr = (new Simplex_tree_interface_full_featured(dereference(ostr.get_ptr()))) + else: + raise TypeError("`other` argument requires to be of type `SimplexTree`, or `None`.") else: self.thisptr = (new Simplex_tree_interface_full_featured()) diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index 2c2e09a2..a8180ce8 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -511,3 +511,7 @@ def test_simplex_tree_deep_copy_constructor(): # test double free del st del st_copy + +def test_simplex_tree_constructor_exception(): + with pytest.raises(TypeError): + st = SimplexTree(other = "Construction from a string shall raise an exception") -- cgit v1.2.3 From 362ac0955656d7c131e042901d6606fdeaab7fc9 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Mon, 14 Feb 2022 23:33:38 +0100 Subject: More details in the doc of weighted rips --- biblio/bibliography.bib | 25 ++++++++++++++++++++++++- src/python/gudhi/weighted_rips_complex.py | 6 ++++-- 2 files changed, 28 insertions(+), 3 deletions(-) (limited to 'src/python/gudhi') diff --git a/biblio/bibliography.bib b/biblio/bibliography.bib index b5afff52..e75e8db2 100644 --- a/biblio/bibliography.bib +++ b/biblio/bibliography.bib @@ -1280,7 +1280,7 @@ year = "2011" publisher={Springer} } -@inproceedings{dtmfiltrations, +@inproceedings{dtmfiltrationsconf, author = {Hirokazu Anai and Fr{\'{e}}d{\'{e}}ric Chazal and Marc Glisse and @@ -1305,6 +1305,29 @@ year = "2011" bibsource = {dblp computer science bibliography, https://dblp.org} } +@InProceedings{dtmfiltrations, +author="Anai, Hirokazu +and Chazal, Fr{\'e}d{\'e}ric +and Glisse, Marc +and Ike, Yuichi +and Inakoshi, Hiroya +and Tinarrage, Rapha{\"e}l +and Umeda, Yuhei", +editor="Baas, Nils A. +and Carlsson, Gunnar E. +and Quick, Gereon +and Szymik, Markus +and Thaule, Marius", +title="DTM-Based Filtrations", +booktitle="Topological Data Analysis", +year="2020", +publisher="Springer International Publishing", +address="Cham", +pages="33--66", +isbn="978-3-030-43408-3", +doi="10.1007/978-3-030-43408-3_2", +} + @InProceedings{edgecollapsesocg2020, author = {Jean-Daniel Boissonnat and Siddharth Pritam}, title = {{Edge Collapse and Persistence of Flag Complexes}}, diff --git a/src/python/gudhi/weighted_rips_complex.py b/src/python/gudhi/weighted_rips_complex.py index 0541572b..16f63c3d 100644 --- a/src/python/gudhi/weighted_rips_complex.py +++ b/src/python/gudhi/weighted_rips_complex.py @@ -12,9 +12,11 @@ from gudhi import SimplexTree class WeightedRipsComplex: """ Class to generate a weighted Rips complex from a distance matrix and weights on vertices, - in the way described in :cite:`dtmfiltrations`. + in the way described in :cite:`dtmfiltrations` with `p=1`. The filtration value of vertex `i` is `2*weights[i]`, + and the filtration value of edge `ij` is `distance_matrix[i][j]+weights[i]+weights[j]`, + or the maximum of the filtrations of its extremities, whichever is largest. Remark that all the filtration values are doubled compared to the definition in the paper - for the consistency with RipsComplex. + for consistency with RipsComplex. """ def __init__(self, distance_matrix, -- cgit v1.2.3 From 4e63e52758daa7ef27782b4f129b7e55fa73de43 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 28 Feb 2022 09:50:08 +0100 Subject: code review: use 'isinstance' instead of 'type' --- src/python/gudhi/simplex_tree.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index e1685ded..711796d4 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -56,7 +56,7 @@ cdef class SimplexTree: def __cinit__(self, other = None): cdef SimplexTree ostr if other: - if type(other) is SimplexTree: + if isinstance(other, SimplexTree): ostr = other self.thisptr = (new Simplex_tree_interface_full_featured(dereference(ostr.get_ptr()))) else: -- cgit v1.2.3 From e7ba8967e9b5c3c7260522003d8f87e643b7912e Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 28 Feb 2022 10:56:47 +0100 Subject: doc review: reformulate the copy notes --- src/python/gudhi/simplex_tree.pyx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 711796d4..91e079aa 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -48,8 +48,8 @@ cdef class SimplexTree: :rtype: SimplexTree :raises TypeError: In case `other` is neither `None`, nor a `SimplexTree`. - :note: If the `SimplexTree` is a copy, it requires :func:`compute_persistence` to be launched again as the - persistence result is not copied. + :note: If the `SimplexTree` is a copy, the persistence information is not copied. If you need it in the clone, + you have to call :func:`compute_persistence` on it even if you had already computed it in the original. """ # The real cython constructor @@ -86,7 +86,8 @@ cdef class SimplexTree: :returns: A simplex tree that is a deep copy of itself. :rtype: SimplexTree - :note: copy requires :func:`compute_persistence` to be launched again as the persistence result is not copied. + :note: The persistence information is not copied. If you need it in the clone, you have to call + :func:`compute_persistence` on it even if you had already computed it in the original. """ stree = SimplexTree() cdef Simplex_tree_interface_full_featured* stree_ptr -- cgit v1.2.3 From 41a976cc85ab36dc2df748b9d6900d77e76b2fa7 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 28 Feb 2022 14:58:24 +0100 Subject: code review: copy simplex tree c++ pointer in a factorized nogil function --- src/python/gudhi/simplex_tree.pyx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 91e079aa..b8fabf78 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -54,11 +54,9 @@ cdef class SimplexTree: # The real cython constructor def __cinit__(self, other = None): - cdef SimplexTree ostr if other: if isinstance(other, SimplexTree): - ostr = other - self.thisptr = (new Simplex_tree_interface_full_featured(dereference(ostr.get_ptr()))) + self.thisptr = _get_copy_intptr(other) else: raise TypeError("`other` argument requires to be of type `SimplexTree`, or `None`.") else: @@ -90,12 +88,7 @@ cdef class SimplexTree: :func:`compute_persistence` on it even if you had already computed it in the original. """ stree = SimplexTree() - cdef Simplex_tree_interface_full_featured* stree_ptr - cdef Simplex_tree_interface_full_featured* self_ptr=self.get_ptr() - with nogil: - stree_ptr = new Simplex_tree_interface_full_featured(dereference(self_ptr)) - - stree.thisptr = (stree_ptr) + stree.thisptr = _get_copy_intptr(self) return stree def __deepcopy__(self): @@ -687,3 +680,6 @@ cdef class SimplexTree: :rtype: bool """ return dereference(self.get_ptr()) == dereference(other.get_ptr()) + +cdef intptr_t _get_copy_intptr(SimplexTree stree) nogil: + return (new Simplex_tree_interface_full_featured(dereference(stree.get_ptr()))) -- cgit v1.2.3 From 924302f092d6d08c59500c5f6e75bcad9416581a Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 28 Mar 2022 08:39:21 +0200 Subject: doc review: remove segfault note as it is no more the case --- src/python/gudhi/simplex_tree.pyx | 3 --- 1 file changed, 3 deletions(-) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 644110c1..fc704358 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -487,9 +487,6 @@ cdef class SimplexTree: with this new simplex (represented as a list of int). If `blocker_func` returns `True`, the simplex is removed, otherwise it is kept. The algorithm then proceeds with the next candidate. - Note that you cannot update the filtration value of the simplex during the evaluation of `blocker_func`, as it - would segfault. - :param max_dim: Expansion maximal dimension value. :type max_dim: int :param blocker_func: Blocker oracle. -- cgit v1.2.3 From f6a45247e9a9f126c214d1b1003ae19fb2cc84a3 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 28 Mar 2022 09:00:23 +0200 Subject: doc review: add a warning about phantom simplices for blocker_func --- src/python/gudhi/simplex_tree.pyx | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/python/gudhi') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index fc704358..a4914184 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -487,6 +487,11 @@ cdef class SimplexTree: with this new simplex (represented as a list of int). If `blocker_func` returns `True`, the simplex is removed, otherwise it is kept. The algorithm then proceeds with the next candidate. + .. warning:: + Several candidates of the same dimension may be inserted simultaneously before calling `block_simplex`, so + if you examine the complex in `block_simplex`, you may hit a few simplices of the same dimension that have + not been vetted by `block_simplex` yet, or have already been rejected but not yet removed. + :param max_dim: Expansion maximal dimension value. :type max_dim: int :param blocker_func: Blocker oracle. -- cgit v1.2.3