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 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 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 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 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 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 f70e6f26f329428184fc5cf935ad4dfc20648bfb Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent 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 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 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 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 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