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(-) 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 5dc55d25f71b16bd1a80f4dc9ebdfad1d861ee0d Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 20 Dec 2020 15:22:22 +0100 Subject: Add tests for BettiCurve2. --- src/python/CMakeLists.txt | 5 +++ .../test/test_betti_curve_representations.py | 41 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100755 src/python/test/test_betti_curve_representations.py diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 5c1402a6..e0e88880 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -512,6 +512,11 @@ if(PYTHONINTERP_FOUND) add_gudhi_py_test(test_representations) endif() + # Betti curves. + if(SCIPY_FOUND) + add_gudhi_py_test(test_betti_curve_representations) + endif() + # Time Delay add_gudhi_py_test(test_time_delay) diff --git a/src/python/test/test_betti_curve_representations.py b/src/python/test/test_betti_curve_representations.py new file mode 100755 index 00000000..779f6d6e --- /dev/null +++ b/src/python/test/test_betti_curve_representations.py @@ -0,0 +1,41 @@ +import numpy as np +import scipy.interpolate + +from gudhi.representations.vector_methods import BettiCurve2 + +def test_betti_curve_is_irregular_betti_curve_followed_by_interpolation(): + m = 10 + n = 1000 + pinf = 0.05 + pzero = 0.05 + res = 100 + success = True + + pds = [] + for i in range(0, m): + pd = np.zeros((n, 2)) + pd[:, 0] = np.random.uniform(0, 10, n) + pd[:, 1] = np.random.uniform(pd[:, 0], 10, n) + pd[np.random.uniform(0, 1, n) < pzero, 0] = 0 + pd[np.random.uniform(0, 1, n) < pinf, 1] = np.inf + pds.append(pd) + + bc = BettiCurve2(None) + bc.fit(pds) + bettis = bc.transform(pds) + + bc2 = BettiCurve2(None) + bettis2 = bc2.fit_transform(pds) + success = success and (bc2.grid_ == bc.grid_).all() + success = success and (bettis2 == bettis).all() + + for i in range(0, m): + grid = np.linspace(pds[i].min(), pds[i].max() + 1, res) + bc_gridded = BettiCurve2(grid) + bettis_gridded = bc_gridded(pds[i]) + + interp = scipy.interpolate.interp1d(bc.grid_, bettis[i, :], kind="previous", fill_value="extrapolate") + bettis_interp = np.array(interp(grid), dtype=int) + success = success and (bettis_interp == bettis_gridded).all() + + assert(success) -- 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(-) 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(-) 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(-) 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 229cef56eebcab9a37bdde10f371c91bdb8cf06c Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 28 Feb 2021 22:56:36 +0100 Subject: Update test --- src/python/test/test_betti_curve_representations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/python/test/test_betti_curve_representations.py b/src/python/test/test_betti_curve_representations.py index 779f6d6e..1c905b82 100755 --- a/src/python/test/test_betti_curve_representations.py +++ b/src/python/test/test_betti_curve_representations.py @@ -30,8 +30,9 @@ def test_betti_curve_is_irregular_betti_curve_followed_by_interpolation(): success = success and (bettis2 == bettis).all() for i in range(0, m): - grid = np.linspace(pds[i].min(), pds[i].max() + 1, res) + grid = np.linspace(pds[i][np.isfinite(pds[i])].min(), pds[i][np.isfinite(pds[i])].max() + 1, res) bc_gridded = BettiCurve2(grid) + bc_gridded.fit([]) bettis_gridded = bc_gridded(pds[i]) interp = scipy.interpolate.interp1d(bc.grid_, bettis[i, :], kind="previous", fill_value="extrapolate") -- cgit v1.2.3 From 5326407d7a787a767dc833a402a71ddf3fda1ed4 Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Sun, 28 Feb 2021 22:59:05 +0100 Subject: Don't wait for end of tests to assert success --- src/python/test/test_betti_curve_representations.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/python/test/test_betti_curve_representations.py b/src/python/test/test_betti_curve_representations.py index 1c905b82..5b95fa2c 100755 --- a/src/python/test/test_betti_curve_representations.py +++ b/src/python/test/test_betti_curve_representations.py @@ -9,7 +9,6 @@ def test_betti_curve_is_irregular_betti_curve_followed_by_interpolation(): pinf = 0.05 pzero = 0.05 res = 100 - success = True pds = [] for i in range(0, m): @@ -26,8 +25,8 @@ def test_betti_curve_is_irregular_betti_curve_followed_by_interpolation(): bc2 = BettiCurve2(None) bettis2 = bc2.fit_transform(pds) - success = success and (bc2.grid_ == bc.grid_).all() - success = success and (bettis2 == bettis).all() + 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) @@ -37,6 +36,4 @@ 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) - success = success and (bettis_interp == bettis_gridded).all() - - assert(success) + assert((bettis_interp == bettis_gridded).all()) -- 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(-) 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 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(+) 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(-) 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(-) 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 cb01ba2a8fa4aba9dc27b9dc62eaaf492150cad0 Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Fri, 30 Apr 2021 15:13:46 +0200 Subject: Update tests to reflect removal of old Betti curve class --- src/python/test/test_betti_curve_representations.py | 2 +- src/python/test/test_representations.py | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/python/test/test_betti_curve_representations.py b/src/python/test/test_betti_curve_representations.py index 475839ee..73f6f34a 100755 --- a/src/python/test/test_betti_curve_representations.py +++ b/src/python/test/test_betti_curve_representations.py @@ -1,7 +1,7 @@ import numpy as np import scipy.interpolate -from gudhi.representations.vector_methods import BettiCurve2 +from gudhi.representations.vector_methods import BettiCurve def test_betti_curve_is_irregular_betti_curve_followed_by_interpolation(): m = 10 diff --git a/src/python/test/test_representations.py b/src/python/test/test_representations.py index 43c914f3..86439655 100755 --- a/src/python/test/test_representations.py +++ b/src/python/test/test_representations.py @@ -63,12 +63,3 @@ def test_dummy_atol(): atol_vectoriser.transform(X=[a, b, c]) -from gudhi.representations.vector_methods import BettiCurve - - -def test_infinity(): - a = np.array([[1.0, 8.0], [2.0, np.inf], [3.0, 4.0]]) - c = BettiCurve(20, [0.0, 10.0])(a) - assert c[1] == 0 - assert c[7] == 3 - assert c[9] == 2 -- 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(-) 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(-) 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 bb07d2bb439e827f232a7504fc2144a4ba5a2478 Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Fri, 4 Jun 2021 14:27:21 +0200 Subject: Actually update test names Fixes incomplete commit cb01ba2a8fa4aba9dc27b9dc62eaaf492150cad0 --- src/python/test/test_betti_curve_representations.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/python/test/test_betti_curve_representations.py b/src/python/test/test_betti_curve_representations.py index 73f6f34a..3e77d760 100755 --- a/src/python/test/test_betti_curve_representations.py +++ b/src/python/test/test_betti_curve_representations.py @@ -19,18 +19,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 = BettiCurve2(None) + bc = BettiCurve(None) bc.fit(pds) bettis = bc.transform(pds) - bc2 = BettiCurve2(None) + bc2 = BettiCurve(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 = BettiCurve2(grid) + bc_gridded = BettiCurve(grid) bc_gridded.fit([]) bettis_gridded = bc_gridded(pds[i]) @@ -41,14 +41,14 @@ 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 = BettiCurve2(random_grid) + bc = BettiCurve(random_grid) bettis = bc.fit_transform([]) assert((bc.grid_ == random_grid).all()) assert((bettis == 0).all()) def test_empty(): - bc = BettiCurve2() + bc = BettiCurve() bettis = bc.fit_transform([]) assert(bc.grid_ == [-np.inf]) assert((bettis == 0).all()) -- 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(-) 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(-) 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 ae85d4a3ea84060e3a209db7c35d6044fec09340 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Thu, 20 Jan 2022 16:37:09 +0100 Subject: Move cmake options in a dedicated file. cmake is no more looking for python and its modules when python option is disabled --- CMakeLists.txt | 4 +- src/CMakeLists.txt | 4 +- src/cmake/modules/GUDHI_modules.cmake | 6 - src/cmake/modules/GUDHI_options.cmake | 5 + .../modules/GUDHI_third_party_libraries.cmake | 175 +++++++++++---------- 5 files changed, 97 insertions(+), 97 deletions(-) create mode 100644 src/cmake/modules/GUDHI_options.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index d0cf6a25..ac877eea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.5) project(GUDHIdev) -include(CMakeGUDHIVersion.txt) - list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/src/cmake/modules/") +include(CMakeGUDHIVersion.txt) +include(GUDHI_options) # Reset cache set(GUDHI_MODULES "" CACHE INTERNAL "GUDHI_MODULES") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8f6a1ccc..8023e04c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.5) project(GUDHI) -include(CMakeGUDHIVersion.txt) - list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules/") +include(CMakeGUDHIVersion.txt) +include(GUDHI_options) set(GUDHI_MODULES "" CACHE INTERNAL "GUDHI_MODULES") set(GUDHI_MISSING_MODULES "" CACHE INTERNAL "GUDHI_MISSING_MODULES") diff --git a/src/cmake/modules/GUDHI_modules.cmake b/src/cmake/modules/GUDHI_modules.cmake index ccaf1ac5..13248f7e 100644 --- a/src/cmake/modules/GUDHI_modules.cmake +++ b/src/cmake/modules/GUDHI_modules.cmake @@ -17,12 +17,6 @@ function(add_gudhi_module file_path) endfunction(add_gudhi_module) -option(WITH_GUDHI_BENCHMARK "Activate/desactivate benchmark compilation" OFF) -option(WITH_GUDHI_EXAMPLE "Activate/desactivate examples compilation and installation" OFF) -option(WITH_GUDHI_PYTHON "Activate/desactivate python module compilation and installation" ON) -option(WITH_GUDHI_TEST "Activate/desactivate examples compilation and installation" ON) -option(WITH_GUDHI_UTILITIES "Activate/desactivate utilities compilation and installation" ON) - if (WITH_GUDHI_BENCHMARK) set(GUDHI_SUB_DIRECTORIES "${GUDHI_SUB_DIRECTORIES};benchmark") endif() diff --git a/src/cmake/modules/GUDHI_options.cmake b/src/cmake/modules/GUDHI_options.cmake new file mode 100644 index 00000000..3cd0a489 --- /dev/null +++ b/src/cmake/modules/GUDHI_options.cmake @@ -0,0 +1,5 @@ +option(WITH_GUDHI_BENCHMARK "Activate/desactivate benchmark compilation" OFF) +option(WITH_GUDHI_EXAMPLE "Activate/desactivate examples compilation and installation" OFF) +option(WITH_GUDHI_PYTHON "Activate/desactivate python module compilation and installation" ON) +option(WITH_GUDHI_TEST "Activate/desactivate examples compilation and installation" ON) +option(WITH_GUDHI_UTILITIES "Activate/desactivate utilities compilation and installation" ON) diff --git a/src/cmake/modules/GUDHI_third_party_libraries.cmake b/src/cmake/modules/GUDHI_third_party_libraries.cmake index 21c9d47b..b316740d 100644 --- a/src/cmake/modules/GUDHI_third_party_libraries.cmake +++ b/src/cmake/modules/GUDHI_third_party_libraries.cmake @@ -93,91 +93,92 @@ add_definitions( -DBOOST_SYSTEM_NO_DEPRECATED ) message(STATUS "boost include dirs:" ${Boost_INCLUDE_DIRS}) message(STATUS "boost library dirs:" ${Boost_LIBRARY_DIRS}) -# Find the correct Python interpreter. -# Can be set with -DPYTHON_EXECUTABLE=/usr/bin/python3 or -DPython_ADDITIONAL_VERSIONS=3 for instance. -find_package( PythonInterp ) - -# find_python_module tries to import module in Python interpreter and to retrieve its version number -# returns ${PYTHON_MODULE_NAME_UP}_VERSION and ${PYTHON_MODULE_NAME_UP}_FOUND -function( find_python_module PYTHON_MODULE_NAME ) - string(TOUPPER ${PYTHON_MODULE_NAME} PYTHON_MODULE_NAME_UP) - execute_process( - COMMAND ${PYTHON_EXECUTABLE} -c "import ${PYTHON_MODULE_NAME}; print(${PYTHON_MODULE_NAME}.__version__)" - RESULT_VARIABLE PYTHON_MODULE_RESULT - OUTPUT_VARIABLE PYTHON_MODULE_VERSION - ERROR_VARIABLE PYTHON_MODULE_ERROR) - if(PYTHON_MODULE_RESULT EQUAL 0) - # Remove all carriage returns as it can be multiline - string(REGEX REPLACE "\n" " " PYTHON_MODULE_VERSION "${PYTHON_MODULE_VERSION}") - message ("++ Python module ${PYTHON_MODULE_NAME} - Version ${PYTHON_MODULE_VERSION} found") - - set(${PYTHON_MODULE_NAME_UP}_VERSION ${PYTHON_MODULE_VERSION} PARENT_SCOPE) - set(${PYTHON_MODULE_NAME_UP}_FOUND TRUE PARENT_SCOPE) - else() - message ("PYTHON_MODULE_NAME = ${PYTHON_MODULE_NAME} - - PYTHON_MODULE_RESULT = ${PYTHON_MODULE_RESULT} - - PYTHON_MODULE_VERSION = ${PYTHON_MODULE_VERSION} - - PYTHON_MODULE_ERROR = ${PYTHON_MODULE_ERROR}") - unset(${PYTHON_MODULE_NAME_UP}_VERSION PARENT_SCOPE) - set(${PYTHON_MODULE_NAME_UP}_FOUND FALSE PARENT_SCOPE) - endif() -endfunction( find_python_module ) - -# For modules that do not define module.__version__ -function( find_python_module_no_version PYTHON_MODULE_NAME ) - string(TOUPPER ${PYTHON_MODULE_NAME} PYTHON_MODULE_NAME_UP) - execute_process( - COMMAND ${PYTHON_EXECUTABLE} -c "import ${PYTHON_MODULE_NAME}" - RESULT_VARIABLE PYTHON_MODULE_RESULT - ERROR_VARIABLE PYTHON_MODULE_ERROR) - if(PYTHON_MODULE_RESULT EQUAL 0) - # Remove carriage return - message ("++ Python module ${PYTHON_MODULE_NAME} found") - set(${PYTHON_MODULE_NAME_UP}_FOUND TRUE PARENT_SCOPE) - else() - message ("PYTHON_MODULE_NAME = ${PYTHON_MODULE_NAME} - - PYTHON_MODULE_RESULT = ${PYTHON_MODULE_RESULT} - - PYTHON_MODULE_ERROR = ${PYTHON_MODULE_ERROR}") - set(${PYTHON_MODULE_NAME_UP}_FOUND FALSE PARENT_SCOPE) +if (WITH_GUDHI_PYTHON) + # Find the correct Python interpreter. + # Can be set with -DPYTHON_EXECUTABLE=/usr/bin/python3 or -DPython_ADDITIONAL_VERSIONS=3 for instance. + find_package( PythonInterp ) + + # find_python_module tries to import module in Python interpreter and to retrieve its version number + # returns ${PYTHON_MODULE_NAME_UP}_VERSION and ${PYTHON_MODULE_NAME_UP}_FOUND + function( find_python_module PYTHON_MODULE_NAME ) + string(TOUPPER ${PYTHON_MODULE_NAME} PYTHON_MODULE_NAME_UP) + execute_process( + COMMAND ${PYTHON_EXECUTABLE} -c "import ${PYTHON_MODULE_NAME}; print(${PYTHON_MODULE_NAME}.__version__)" + RESULT_VARIABLE PYTHON_MODULE_RESULT + OUTPUT_VARIABLE PYTHON_MODULE_VERSION + ERROR_VARIABLE PYTHON_MODULE_ERROR) + if(PYTHON_MODULE_RESULT EQUAL 0) + # Remove all carriage returns as it can be multiline + string(REGEX REPLACE "\n" " " PYTHON_MODULE_VERSION "${PYTHON_MODULE_VERSION}") + message ("++ Python module ${PYTHON_MODULE_NAME} - Version ${PYTHON_MODULE_VERSION} found") + + set(${PYTHON_MODULE_NAME_UP}_VERSION ${PYTHON_MODULE_VERSION} PARENT_SCOPE) + set(${PYTHON_MODULE_NAME_UP}_FOUND TRUE PARENT_SCOPE) + else() + message ("PYTHON_MODULE_NAME = ${PYTHON_MODULE_NAME} + - PYTHON_MODULE_RESULT = ${PYTHON_MODULE_RESULT} + - PYTHON_MODULE_VERSION = ${PYTHON_MODULE_VERSION} + - PYTHON_MODULE_ERROR = ${PYTHON_MODULE_ERROR}") + unset(${PYTHON_MODULE_NAME_UP}_VERSION PARENT_SCOPE) + set(${PYTHON_MODULE_NAME_UP}_FOUND FALSE PARENT_SCOPE) + endif() + endfunction( find_python_module ) + + # For modules that do not define module.__version__ + function( find_python_module_no_version PYTHON_MODULE_NAME ) + string(TOUPPER ${PYTHON_MODULE_NAME} PYTHON_MODULE_NAME_UP) + execute_process( + COMMAND ${PYTHON_EXECUTABLE} -c "import ${PYTHON_MODULE_NAME}" + RESULT_VARIABLE PYTHON_MODULE_RESULT + ERROR_VARIABLE PYTHON_MODULE_ERROR) + if(PYTHON_MODULE_RESULT EQUAL 0) + # Remove carriage return + message ("++ Python module ${PYTHON_MODULE_NAME} found") + set(${PYTHON_MODULE_NAME_UP}_FOUND TRUE PARENT_SCOPE) + else() + message ("PYTHON_MODULE_NAME = ${PYTHON_MODULE_NAME} + - PYTHON_MODULE_RESULT = ${PYTHON_MODULE_RESULT} + - PYTHON_MODULE_ERROR = ${PYTHON_MODULE_ERROR}") + set(${PYTHON_MODULE_NAME_UP}_FOUND FALSE PARENT_SCOPE) + endif() + endfunction( find_python_module_no_version ) + + if( PYTHONINTERP_FOUND ) + find_python_module("cython") + find_python_module("pytest") + find_python_module("matplotlib") + find_python_module("numpy") + find_python_module("scipy") + find_python_module("sphinx") + find_python_module("sklearn") + find_python_module("ot") + find_python_module("pybind11") + find_python_module("torch") + find_python_module("pykeops") + find_python_module("eagerpy") + find_python_module_no_version("hnswlib") + find_python_module("tensorflow") + find_python_module("sphinx_paramlinks") + find_python_module_no_version("python_docs_theme") endif() -endfunction( find_python_module_no_version ) - -if( PYTHONINTERP_FOUND ) - find_python_module("cython") - find_python_module("pytest") - find_python_module("matplotlib") - find_python_module("numpy") - find_python_module("scipy") - find_python_module("sphinx") - find_python_module("sklearn") - find_python_module("ot") - find_python_module("pybind11") - find_python_module("torch") - find_python_module("pykeops") - find_python_module("eagerpy") - find_python_module_no_version("hnswlib") - find_python_module("tensorflow") - find_python_module("sphinx_paramlinks") - find_python_module_no_version("python_docs_theme") -endif() - -if(NOT GUDHI_PYTHON_PATH) - message(FATAL_ERROR "ERROR: GUDHI_PYTHON_PATH is not valid.") -endif(NOT GUDHI_PYTHON_PATH) - -option(WITH_GUDHI_PYTHON_RUNTIME_LIBRARY_DIRS "Build with setting runtime_library_dirs. Usefull when setting rpath is not allowed" ON) - -if(PYTHONINTERP_FOUND AND CYTHON_FOUND) - if(SPHINX_FOUND) - # Documentation generation is available through sphinx - find_program( SPHINX_PATH sphinx-build ) - - if(NOT SPHINX_PATH) - if(PYTHON_VERSION_MAJOR EQUAL 3) - # In Python3, just hack sphinx-build if it does not exist - set(SPHINX_PATH "${PYTHON_EXECUTABLE}" "-m" "sphinx.cmd.build") - endif(PYTHON_VERSION_MAJOR EQUAL 3) - endif(NOT SPHINX_PATH) - endif(SPHINX_FOUND) -endif(PYTHONINTERP_FOUND AND CYTHON_FOUND) - + + if(NOT GUDHI_PYTHON_PATH) + message(FATAL_ERROR "ERROR: GUDHI_PYTHON_PATH is not valid.") + endif(NOT GUDHI_PYTHON_PATH) + + option(WITH_GUDHI_PYTHON_RUNTIME_LIBRARY_DIRS "Build with setting runtime_library_dirs. Usefull when setting rpath is not allowed" ON) + + if(PYTHONINTERP_FOUND AND CYTHON_FOUND) + if(SPHINX_FOUND) + # Documentation generation is available through sphinx + find_program( SPHINX_PATH sphinx-build ) + + if(NOT SPHINX_PATH) + if(PYTHON_VERSION_MAJOR EQUAL 3) + # In Python3, just hack sphinx-build if it does not exist + set(SPHINX_PATH "${PYTHON_EXECUTABLE}" "-m" "sphinx.cmd.build") + endif(PYTHON_VERSION_MAJOR EQUAL 3) + endif(NOT SPHINX_PATH) + endif(SPHINX_FOUND) + endif(PYTHONINTERP_FOUND AND CYTHON_FOUND) +endif (WITH_GUDHI_PYTHON) \ No newline at end of file -- cgit v1.2.3 From a7ec48c56b16c1197cfad83706e643b07d2d6b56 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau <10407034+VincentRouvreau@users.noreply.github.com> Date: Fri, 21 Jan 2022 09:50:47 +0100 Subject: Experiment azure ci instead of appveyor for windows (#574) * Remove appveyor build Windows builds and tests under azure: * Add windows build in azure yaml file and rename pipelines * Remove '-j 4' as not used * Separate c++ and python compilation (tests are easier to be launched under src/python) * Explain the change in tests_strategy --- .appveyor.yml | 80 ------------------------------- .github/for_maintainers/tests_strategy.md | 10 ++-- azure-pipelines.yml | 75 +++++++++++++++++++++++------ 3 files changed, 66 insertions(+), 99 deletions(-) delete mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 33458a28..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,80 +0,0 @@ -image: - - Visual Studio 2019 - -build: - parallel: true - verbosity: detailed - -configuration: - - Release - -environment: - # update the vcpkg cache even if build fails - # APPVEYOR_SAVE_CACHE_ON_ERROR: true - PYTHON: "C:\\Python39-x64" - PYTHONPATH: "C:\\Python39-x64\\lib\\site-packages" - CMAKE_VCPKG_FLAGS: -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_TOOLCHAIN_FILE=c:\Tools\vcpkg\scripts\buildsystems\vcpkg.cmake - - matrix: - - target: Examples - CMAKE_FLAGS: -DWITH_GUDHI_EXAMPLE=ON -DWITH_GUDHI_TEST=OFF -DWITH_GUDHI_UTILITIES=OFF -DWITH_GUDHI_PYTHON=OFF - - - target: UnitaryTests - CMAKE_FLAGS: -DWITH_GUDHI_EXAMPLE=OFF -DWITH_GUDHI_TEST=ON -DWITH_GUDHI_UTILITIES=OFF -DWITH_GUDHI_PYTHON=OFF - - - target: Utilities - CMAKE_FLAGS: -DWITH_GUDHI_EXAMPLE=OFF -DWITH_GUDHI_TEST=OFF -DWITH_GUDHI_UTILITIES=ON -DWITH_GUDHI_PYTHON=OFF - - - target: Python - CMAKE_FLAGS: -DWITH_GUDHI_EXAMPLE=OFF -DWITH_GUDHI_TEST=OFF -DWITH_GUDHI_UTILITIES=OFF -DWITH_GUDHI_PYTHON=ON - - -#cache: -# - c:\Tools\vcpkg\installed -# - '%LOCALAPPDATA%\pip\Cache' - -init: - - echo %target% - -install: - - git submodule update --init - - vcpkg update - - vcpkg remove --outdated - - vcpkg upgrade --no-dry-run - - vcpkg install boost-filesystem:x64-windows boost-test:x64-windows boost-program-options:x64-windows tbb:x64-windows eigen3:x64-windows cgal:x64-windows - - dir "C:\Tools\vcpkg\installed\x64-windows\bin\" - - vcpkg integrate install - - CALL "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64 - - "set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - python --version - - pip --version - - python -m pip install --upgrade pip - - python -m pip install --upgrade setuptools - - python -m pip install -r ext\gudhi-deploy\build-requirements.txt - # No PyKeOps on windows, let's workaround this one. - - for /F "tokens=*" %%A in (ext\gudhi-deploy\test-requirements.txt) do python -m pip install %%A - - dir "c:\python39-x64\lib\site-packages" - - dir "%LOCALAPPDATA%\pip\Cache" - - python -c "from scipy import spatial; print(spatial.cKDTree)" - -build_script: - - mkdir build - - cd build - - cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=Release %CMAKE_FLAGS% %CMAKE_VCPKG_FLAGS% .. - - if [%target%]==[Python] ( - cd src\python & - dir . & - type setup.py & - copy "C:\Tools\vcpkg\installed\x64-windows\bin\mpfr-6.dll" ".\gudhi\" & - copy "C:\Tools\vcpkg\installed\x64-windows\bin\gmp.dll" ".\gudhi\" & - copy "C:\Tools\vcpkg\installed\x64-windows\bin\tbb.dll" ".\gudhi\" & - copy "C:\Tools\vcpkg\installed\x64-windows\bin\tbbmalloc.dll" ".\gudhi\" & - python setup.py build_ext --inplace & - SET PYTHONPATH=%CD%;%PYTHONPATH% & - echo %PYTHONPATH% & - ctest -j 1 --output-on-failure -C Release - ) else ( - dir . & - MSBuild GUDHIdev.sln /m /p:Configuration=Release /p:Platform=x64 & - ctest -j 1 --output-on-failure -C Release -E diff_files - ) diff --git a/.github/for_maintainers/tests_strategy.md b/.github/for_maintainers/tests_strategy.md index 9c181740..c25acf9b 100644 --- a/.github/for_maintainers/tests_strategy.md +++ b/.github/for_maintainers/tests_strategy.md @@ -39,22 +39,24 @@ docker push gudhi/ci_for_gudhi_wo_cgal:latest ### Windows -The compilations has been seperated by categories to be parallelized, but I don't know why builds are not run in parallel: +The compilations are not parallelized, as installation time (about 30 minutes) is too much compare to +build and tests timings (about 30 minutes). Builds and tests include: * examples (C++) * tests (C++) * utils (C++) * python -Doxygen (C++) is not tested. -(cf. `.appveyor.yml`) +Doxygen (C++) is not generated. +(cf. `azure-pipelines.yml`) C++ third parties installation are done thanks to [vcpkg](https://github.com/microsoft/vcpkg/). In case of installation issue, check in [vcpkg issues](https://github.com/microsoft/vcpkg/issues). ### OSx -The compilations has been seperated by categories to be parallelized: +The compilations are not parallelized, but they should, as installation time (about 4 minutes) is +negligeable compare to build and tests timings (about 30 minutes). Builds and tests include: * examples (C++) * tests (C++) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a96323fd..21664244 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,26 +1,26 @@ jobs: - - job: 'Test' - displayName: "Build and test" + - job: 'OSx' + displayName: "Build and test OSx" timeoutInMinutes: 0 cancelTimeoutInMinutes: 60 pool: vmImage: macOS-10.15 variables: - pythonVersion: '3.6' + pythonVersion: '3.7' cmakeBuildType: Release steps: - - bash: echo "##vso[task.prependpath]$CONDA/bin" - displayName: Add conda to PATH - - - bash: sudo conda create --yes --quiet --name gudhi_build_env - displayName: Create Anaconda environment - + # Use a specific Python version + - task: UsePythonVersion@0 + displayName: Use Python $(pythonVersion) + inputs: + versionSpec: $(pythonVersion) + addToPath: true + architecture: 'x64' + - bash: | - source activate gudhi_build_env git submodule update --init - sudo conda install --yes --quiet --name gudhi_build_env python=$(pythonVersion) python -m pip install --user -r ext/gudhi-deploy/build-requirements.txt python -m pip install --user -r ext/gudhi-deploy/test-requirements.txt python -m pip uninstall -y pykeops @@ -28,11 +28,56 @@ jobs: brew install graphviz doxygen boost eigen gmp mpfr tbb cgal || true displayName: 'Install build dependencies' - bash: | - source activate gudhi_build_env mkdir build cd build - cmake -DCMAKE_BUILD_TYPE:STRING=$(cmakeBuildType) -DWITH_GUDHI_TEST=ON -DWITH_GUDHI_UTILITIES=ON -DWITH_GUDHI_PYTHON=ON -DPython_ADDITIONAL_VERSIONS=3 .. - make -j 4 + cmake -DCMAKE_BUILD_TYPE:STRING=$(cmakeBuildType) -DWITH_GUDHI_TEST=ON -DWITH_GUDHI_UTILITIES=ON -DWITH_GUDHI_PYTHON=ON .. + make make doxygen - ctest -j 4 --output-on-failure # -E sphinx remove sphinx build as it fails + ctest --output-on-failure displayName: 'Build, test and documentation generation' + + - job: 'Windows' + displayName: "Build and test Windows" + timeoutInMinutes: 0 + cancelTimeoutInMinutes: 60 + pool: + vmImage: windows-latest + variables: + pythonVersion: '3.7' + cmakeVcpkgFlags: -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_TOOLCHAIN_FILE=c:\vcpkg\scripts\buildsystems\vcpkg.cmake + cmakeFlags: -DWITH_GUDHI_EXAMPLE=ON -DWITH_GUDHI_TEST=ON -DWITH_GUDHI_UTILITIES=ON -DWITH_GUDHI_PYTHON=OFF + + steps: + # Use a specific Python version + - task: UsePythonVersion@0 + displayName: Use Python $(pythonVersion) + inputs: + versionSpec: $(pythonVersion) + addToPath: true + architecture: 'x64' + + - script: | + git submodule update --init + python -m pip install --user -r ext/gudhi-deploy/build-requirements.txt + # No PyKeOps on windows, let's workaround this one. + for /F "tokens=*" %%A in (ext\gudhi-deploy\test-requirements.txt) do python -m pip install %%A + vcpkg install boost-filesystem:x64-windows boost-test:x64-windows boost-program-options:x64-windows tbb:x64-windows eigen3:x64-windows cgal:x64-windows + displayName: 'Install build dependencies' + - script: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" amd64 + mkdir build + cd build + cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=Release $(cmakeVcpkgFlags) $(cmakeFlags) .. + MSBuild GUDHIdev.sln /m /p:Configuration=Release /p:Platform=x64 + ctest --output-on-failure -C Release -E diff_files + cmake -DWITH_GUDHI_PYTHON=ON . + cd src\python + copy "C:\vcpkg\installed\x64-windows\bin\mpfr-6.dll" ".\gudhi\" + copy "C:\vcpkg\installed\x64-windows\bin\gmp.dll" ".\gudhi\" + copy "C:\vcpkg\installed\x64-windows\bin\tbb.dll" ".\gudhi\" + copy "C:\vcpkg\installed\x64-windows\bin\tbbmalloc.dll" ".\gudhi\" + python setup.py build_ext --inplace + SET PYTHONPATH=%CD%;%PYTHONPATH% + echo %PYTHONPATH% + ctest --output-on-failure -C Release + displayName: 'Build and test' -- cgit v1.2.3 From 0a61efdf924b228e683af6d7ed1de0e3387292c0 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Fri, 21 Jan 2022 17:56:17 +0100 Subject: [skip ci] Add exact betti curve in release note --- .github/next_release.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/next_release.md b/.github/next_release.md index 0a8b2bbb..d36c55c6 100644 --- a/.github/next_release.md +++ b/.github/next_release.md @@ -6,8 +6,8 @@ We are now using GitHub to develop the GUDHI library, do not hesitate to [fork t Below is a list of changes made since GUDHI 3.5.0: -- [Module](link) - - ... +- [Representations](https://gudhi.inria.fr/python/latest/representations.html#gudhi.representations.vector_methods.BettiCurve) + - A more flexible Betti curve class capable of computing exact curves - [Module](link) - ... -- cgit v1.2.3 From cf38c8cff06272fd3399ab68bbe31fe1f73b6d7f Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Fri, 21 Jan 2022 17:56:46 +0100 Subject: [skip ci] Remove Appveyor badge on readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 279953e1..664483ee 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -[![OSx on Azure](https://dev.azure.com/GUDHI/gudhi-devel/_apis/build/status/GUDHI.gudhi-devel?branchName=master)](https://dev.azure.com/GUDHI/gudhi-devel/_build/latest?definitionId=1&branchName=master) +[![OSx and Win on Azure](https://dev.azure.com/GUDHI/gudhi-devel/_apis/build/status/GUDHI.gudhi-devel?branchName=master)](https://dev.azure.com/GUDHI/gudhi-devel/_build/latest?definitionId=1&branchName=master) [![Linux on CircleCI](https://circleci.com/gh/GUDHI/gudhi-devel/tree/master.svg?style=svg)](https://circleci.com/gh/GUDHI/gudhi-devel/tree/master) -[![Win on Appveyor](https://ci.appveyor.com/api/projects/status/976j2uut8xgalvx2/branch/master?svg=true)](https://ci.appveyor.com/project/GUDHI/gudhi-devel/branch/master) [![Anaconda Cloud](https://anaconda.org/conda-forge/gudhi/badges/version.svg)](https://anaconda.org/conda-forge/gudhi) [![Anaconda downloads](https://anaconda.org/conda-forge/gudhi/badges/downloads.svg)](https://anaconda.org/conda-forge/gudhi) -- cgit v1.2.3