From 9c45abcdf165519c58d59556dea74fd9f27c8396 Mon Sep 17 00:00:00 2001 From: martinroyer Date: Mon, 8 Jun 2020 15:56:34 +0200 Subject: ATOL introduction as finite vectorisation method --- src/python/gudhi/representations/vector_methods.py | 128 ++++++++++++++++++++- 1 file changed, 125 insertions(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 46fee086..df66ffc3 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -1,16 +1,17 @@ # 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 +# Author(s): Mathieu Carrière, Martin Royer # -# Copyright (C) 2018-2019 Inria +# Copyright (C) 2018-2020 Inria # # Modification(s): -# - YYYY/MM Author: Description of the modification +# - 2020/06 Martin: ATOL integration import numpy as np from sklearn.base import BaseEstimator, TransformerMixin from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler from sklearn.neighbors import DistanceMetric +from sklearn.metrics import pairwise from .preprocessing import DiagramScaler, BirthPersistenceTransform @@ -574,3 +575,124 @@ class ComplexPolynomial(BaseEstimator, TransformerMixin): numpy array with shape (**threshold**): output complex vector of coefficients. """ return self.fit_transform([diag])[0,:] + +def _lapl_contrast(measure, centers, inertias, eps=1e-8): + """contrast function for vectorising `measure` in ATOL""" + return np.exp(-np.sqrt(pairwise.pairwise_distances(measure, Y=centers) / (inertias + eps))) + +def _gaus_contrast(measure, centers, inertias, eps=1e-8): + """contrast function for vectorising `measure` in ATOL""" + return np.exp(-pairwise.pairwise_distances(measure, Y=centers) / (inertias + eps)) + +def _indicator_contrast(diags, centers, inertias, eps=1e-8): + """contrast function for vectorising `measure` in ATOL""" + pair_dist = pairwise.pairwise_distances(diags, Y=centers) + flat_circ = (pair_dist < (inertias+eps)).astype(int) + robe_curve = np.positive((2-pair_dist/(inertias+eps))*((inertias+eps) < pair_dist).astype(int)) + return flat_circ + robe_curve + +def _cloud_weighting(measure): + """automatic uniform weighting with mass 1 for `measure` in ATOL""" + return np.ones(shape=measure.shape[0]) + +def _iidproba_weighting(measure): + """automatic uniform weighting with mass 1/N for `measure` in ATOL""" + return np.ones(shape=measure.shape[0]) / measure.shape[0] + +class Atol(BaseEstimator, TransformerMixin): + """ + This class allows to vectorise measures (e.g. point clouds, persistence diagrams, etc) after a quantisation step. + + ATOL paper: https://arxiv.org/abs/1909.13472 + """ + def __init__(self, quantiser, weighting_method="cloud", contrast="gaus"): + """ + Constructor for the Atol measure vectorisation class. + + Parameters: + quantiser (Object): Object with `fit` (sklearn API consistent) and `cluster_centers` and `n_clusters` + attributes (default: MiniBatchKMeans()). This object will be fitted by the function `fit`. + weighting_method (function): constant generic function for weighting the measure points + choose from {"cloud", "iidproba"} + (default: constant function, i.e. the measure is seen as a point cloud by default). + This will have no impact if weights are provided along with measures all the way: `fit` and `transform`. + contrast (string): constant function for evaluating proximity of a measure with respect to centers + choose from {"gaus", "lapl", "indi"} + (default: laplacian contrast function, see page 3 in the ATOL paper). + """ + self.quantiser = quantiser + self.contrast = { + "gaus": _gaus_contrast, + "lapl": _lapl_contrast, + "indi": _indicator_contrast, + }.get(contrast, _gaus_contrast) + self.centers = np.ones(shape=(self.quantiser.n_clusters, 2))*np.inf + self.inertias = np.full(self.quantiser.n_clusters, np.nan) + self.weighting_method = { + "cloud" : _cloud_weighting, + "iidproba": _iidproba_weighting, + }.get(weighting_method, _cloud_weighting) + + def fit(self, X, y=None, sample_weight=None): + """ + Calibration step: fit centers to the sample measures and derive inertias between centers. + + Parameters: + X (list N x d numpy arrays): input measures in R^d from which to learn center locations and inertias + (measures can have different N). + y: Ignored, present for API consistency by convention. + sample_weight (list of numpy arrays): weights for each measure point in X, optional. + If None, the object's weighting_method will be used. + + Returns: + self + """ + if not hasattr(self.quantiser, 'fit'): + raise TypeError("quantiser %s has no `fit` attribute." % (self.quantiser)) + if len(X) < self.quantiser.n_clusters: + # in case there are not enough observations for fitting the quantiser, we add random points in [0, 1]^2 + # @Martin: perhaps this behaviour is to be externalised and a warning should be raised instead + random_points = np.random.rand(self.quantiser.n_clusters-len(X), X[0].shape[1]) + X.append(random_points) + if sample_weight is None: + sample_weight = np.concatenate([self.weighting_method(measure) for measure in X]) + + measures_concat = np.concatenate(X) + self.quantiser.fit(X=measures_concat, sample_weight=sample_weight) + self.centers = self.quantiser.cluster_centers_ + labels = np.argmin(pairwise.pairwise_distances(measures_concat, Y=self.centers), axis=1) + dist_centers = pairwise.pairwise_distances(self.centers) + np.fill_diagonal(dist_centers, np.inf) + self.inertias = np.min(dist_centers, axis=0)/2 + return self + + def __call__(self, measure, sample_weight=None): + """ + Apply measure vectorisation on a single measure. + + Parameters: + measure (n x d numpy array): input measure in R^d. + + Returns: + numpy array in R^self.quantiser.n_clusters. + """ + if sample_weight is None: + sample_weight = self.weighting_method(measure) + return np.sum(sample_weight * self.contrast(measure, self.centers, self.inertias.T).T, axis=1) + + def transform(self, X, sample_weight=None): + """ + Apply measure vectorisation on a list of measures. + + Parameters: + X (list N x d numpy arrays): input measures in R^d from which to learn center locations and inertias + (measures can have different N). + sample_weight (list of numpy arrays): weights for each measure point in X, optional. + If None, the object's weighting_method will be used. + + Returns: + numpy array with shape (number of measures) x (self.quantiser.n_clusters). + """ + if sample_weight is None: + sample_weight = [self.weighting_method(measure) for measure in X] + return np.stack([self(measure, sample_weight=weight) for measure, weight in zip(X, sample_weight)]) -- cgit v1.2.3 From 9b4de0e29a01552b4bb3f47fe0d3f01f5601c000 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Tue, 9 Jun 2020 08:42:30 +0200 Subject: Apply suggestions from code review --- src/python/gudhi/representations/vector_methods.py | 45 ++++++++++++++++------ 1 file changed, 33 insertions(+), 12 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index df66ffc3..a09b9356 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -577,26 +577,26 @@ class ComplexPolynomial(BaseEstimator, TransformerMixin): return self.fit_transform([diag])[0,:] def _lapl_contrast(measure, centers, inertias, eps=1e-8): - """contrast function for vectorising `measure` in ATOL""" + """contrast function for vectorising `measure` in ATOL""" return np.exp(-np.sqrt(pairwise.pairwise_distances(measure, Y=centers) / (inertias + eps))) def _gaus_contrast(measure, centers, inertias, eps=1e-8): - """contrast function for vectorising `measure` in ATOL""" + """contrast function for vectorising `measure` in ATOL""" return np.exp(-pairwise.pairwise_distances(measure, Y=centers) / (inertias + eps)) def _indicator_contrast(diags, centers, inertias, eps=1e-8): - """contrast function for vectorising `measure` in ATOL""" + """contrast function for vectorising `measure` in ATOL""" pair_dist = pairwise.pairwise_distances(diags, Y=centers) flat_circ = (pair_dist < (inertias+eps)).astype(int) robe_curve = np.positive((2-pair_dist/(inertias+eps))*((inertias+eps) < pair_dist).astype(int)) return flat_circ + robe_curve def _cloud_weighting(measure): - """automatic uniform weighting with mass 1 for `measure` in ATOL""" + """automatic uniform weighting with mass 1 for `measure` in ATOL""" return np.ones(shape=measure.shape[0]) def _iidproba_weighting(measure): - """automatic uniform weighting with mass 1/N for `measure` in ATOL""" + """automatic uniform weighting with mass 1/N for `measure` in ATOL""" return np.ones(shape=measure.shape[0]) / measure.shape[0] class Atol(BaseEstimator, TransformerMixin): @@ -611,20 +611,41 @@ class Atol(BaseEstimator, TransformerMixin): Parameters: quantiser (Object): Object with `fit` (sklearn API consistent) and `cluster_centers` and `n_clusters` - attributes (default: MiniBatchKMeans()). This object will be fitted by the function `fit`. - weighting_method (function): constant generic function for weighting the measure points + attributes. This object will be fitted by the function `fit`. + weighting_method (string): constant generic function for weighting the measure points choose from {"cloud", "iidproba"} (default: constant function, i.e. the measure is seen as a point cloud by default). This will have no impact if weights are provided along with measures all the way: `fit` and `transform`. contrast (string): constant function for evaluating proximity of a measure with respect to centers - choose from {"gaus", "lapl", "indi"} + choose from {"gaussian", "laplacian", "indicator"} (default: laplacian contrast function, see page 3 in the ATOL paper). - """ + + Example + -------- + >>> from sklearn.cluster import KMeans + >>> import numpy as np + >>> a = np.array([[1, 2, 4], [1, 4, 0], [1, 0, 4]]) + >>> b = np.array([[4, 2, 0], [4, 4, 0], [4, 0, 2]]) + >>> c = np.array([[3, 2, -1], [1, 2, -1]]) + >>> atol_vectoriser = Atol(quantiser=KMeans(n_clusters=2)) + >>> atol_vectoriser.fit(X=[a, b, c]) + >>> atol_vectoriser.centers + array([[ 2.6 , 2.8 , -0.4 ], + [ 2. , 0.66666667, 3.33333333]]) + >>> atol_vectoriser(a) + array([0.58394704, 1.0769395 ]) + >>> atol_vectoriser(c) + array([1.02816136, 0.23559623]) + >>> atol_vectoriser.transform(X=[a, b, c]) + array([[0.58394704, 1.0769395 ], + [1.04696684, 0.56203292], + [1.02816136, 0.23559623]]) + """ self.quantiser = quantiser self.contrast = { - "gaus": _gaus_contrast, - "lapl": _lapl_contrast, - "indi": _indicator_contrast, + "gaussian": _gaus_contrast, + "laplacian": _lapl_contrast, + "indicator": _indicator_contrast, }.get(contrast, _gaus_contrast) self.centers = np.ones(shape=(self.quantiser.n_clusters, 2))*np.inf self.inertias = np.full(self.quantiser.n_clusters, np.nan) -- cgit v1.2.3 From 285919ad4a19c6bf9ec11355cd32bc4b39014365 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Tue, 9 Jun 2020 09:12:18 +0200 Subject: fix minimal example --- src/python/gudhi/representations/vector_methods.py | 31 +++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index a09b9356..f77338b7 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -604,25 +604,11 @@ class Atol(BaseEstimator, TransformerMixin): This class allows to vectorise measures (e.g. point clouds, persistence diagrams, etc) after a quantisation step. ATOL paper: https://arxiv.org/abs/1909.13472 - """ - def __init__(self, quantiser, weighting_method="cloud", contrast="gaus"): - """ - Constructor for the Atol measure vectorisation class. - - Parameters: - quantiser (Object): Object with `fit` (sklearn API consistent) and `cluster_centers` and `n_clusters` - attributes. This object will be fitted by the function `fit`. - weighting_method (string): constant generic function for weighting the measure points - choose from {"cloud", "iidproba"} - (default: constant function, i.e. the measure is seen as a point cloud by default). - This will have no impact if weights are provided along with measures all the way: `fit` and `transform`. - contrast (string): constant function for evaluating proximity of a measure with respect to centers - choose from {"gaussian", "laplacian", "indicator"} - (default: laplacian contrast function, see page 3 in the ATOL paper). Example -------- >>> from sklearn.cluster import KMeans + >>> from gudhi.representations.vector_methods import Atol >>> import numpy as np >>> a = np.array([[1, 2, 4], [1, 4, 0], [1, 0, 4]]) >>> b = np.array([[4, 2, 0], [4, 4, 0], [4, 0, 2]]) @@ -641,6 +627,21 @@ class Atol(BaseEstimator, TransformerMixin): [1.04696684, 0.56203292], [1.02816136, 0.23559623]]) """ + def __init__(self, quantiser, weighting_method="cloud", contrast="gaus"): + """ + Constructor for the Atol measure vectorisation class. + + Parameters: + quantiser (Object): Object with `fit` (sklearn API consistent) and `cluster_centers` and `n_clusters` + attributes. This object will be fitted by the function `fit`. + weighting_method (string): constant generic function for weighting the measure points + choose from {"cloud", "iidproba"} + (default: constant function, i.e. the measure is seen as a point cloud by default). + This will have no impact if weights are provided along with measures all the way: `fit` and `transform`. + contrast (string): constant function for evaluating proximity of a measure with respect to centers + choose from {"gaussian", "laplacian", "indicator"} + (default: laplacian contrast function, see page 3 in the ATOL paper). + """ self.quantiser = quantiser self.contrast = { "gaussian": _gaus_contrast, -- cgit v1.2.3 From 5d5a2b1a3b6a2d3d2dc061e6e9c940677e782c80 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Tue, 9 Jun 2020 09:13:28 +0200 Subject: Update src/python/gudhi/representations/vector_methods.py --- src/python/gudhi/representations/vector_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index f77338b7..16e91812 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -641,7 +641,7 @@ class Atol(BaseEstimator, TransformerMixin): contrast (string): constant function for evaluating proximity of a measure with respect to centers choose from {"gaussian", "laplacian", "indicator"} (default: laplacian contrast function, see page 3 in the ATOL paper). - """ + """ self.quantiser = quantiser self.contrast = { "gaussian": _gaus_contrast, -- cgit v1.2.3 From 588e7127d1616e40bf7e3de7e7797b54aee137da Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Tue, 9 Jun 2020 09:42:40 +0200 Subject: tweak test results from doc --- src/python/gudhi/representations/vector_methods.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 16e91812..d3b85636 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -614,18 +614,17 @@ class Atol(BaseEstimator, TransformerMixin): >>> b = np.array([[4, 2, 0], [4, 4, 0], [4, 0, 2]]) >>> c = np.array([[3, 2, -1], [1, 2, -1]]) >>> atol_vectoriser = Atol(quantiser=KMeans(n_clusters=2)) - >>> atol_vectoriser.fit(X=[a, b, c]) - >>> atol_vectoriser.centers + >>> atol_vectoriser.fit(X=[a, b, c]).centers array([[ 2.6 , 2.8 , -0.4 ], - [ 2. , 0.66666667, 3.33333333]]) + [ 2. , 0.66666667, 3.33333333]]) >>> atol_vectoriser(a) array([0.58394704, 1.0769395 ]) >>> atol_vectoriser(c) array([1.02816136, 0.23559623]) >>> atol_vectoriser.transform(X=[a, b, c]) array([[0.58394704, 1.0769395 ], - [1.04696684, 0.56203292], - [1.02816136, 0.23559623]]) + [1.04696684, 0.56203292], + [1.02816136, 0.23559623]]) """ def __init__(self, quantiser, weighting_method="cloud", contrast="gaus"): """ -- cgit v1.2.3 From 7eed21c364e244df7fceae11ce9d1c319db8bec9 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Tue, 9 Jun 2020 11:21:29 +0200 Subject: Update src/python/gudhi/representations/vector_methods.py --- src/python/gudhi/representations/vector_methods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index d3b85636..0a26a8e5 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -626,7 +626,7 @@ class Atol(BaseEstimator, TransformerMixin): [1.04696684, 0.56203292], [1.02816136, 0.23559623]]) """ - def __init__(self, quantiser, weighting_method="cloud", contrast="gaus"): + def __init__(self, quantiser, weighting_method="cloud", contrast="gaussian"): """ Constructor for the Atol measure vectorisation class. @@ -639,7 +639,7 @@ class Atol(BaseEstimator, TransformerMixin): This will have no impact if weights are provided along with measures all the way: `fit` and `transform`. contrast (string): constant function for evaluating proximity of a measure with respect to centers choose from {"gaussian", "laplacian", "indicator"} - (default: laplacian contrast function, see page 3 in the ATOL paper). + (default: gaussian contrast function, see page 3 in the ATOL paper). """ self.quantiser = quantiser self.contrast = { -- cgit v1.2.3 From a22b48e00ca858c0e9c300cee87f265d70aeecc7 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Tue, 9 Jun 2020 12:23:20 +0200 Subject: remove randomness in example --- src/python/gudhi/representations/vector_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 0a26a8e5..98cd6153 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -613,7 +613,7 @@ class Atol(BaseEstimator, TransformerMixin): >>> a = np.array([[1, 2, 4], [1, 4, 0], [1, 0, 4]]) >>> b = np.array([[4, 2, 0], [4, 4, 0], [4, 0, 2]]) >>> c = np.array([[3, 2, -1], [1, 2, -1]]) - >>> atol_vectoriser = Atol(quantiser=KMeans(n_clusters=2)) + >>> atol_vectoriser = Atol(quantiser=KMeans(n_clusters=2, random_state=202006)) >>> atol_vectoriser.fit(X=[a, b, c]).centers array([[ 2.6 , 2.8 , -0.4 ], [ 2. , 0.66666667, 3.33333333]]) -- cgit v1.2.3 From 3d126356fd3fcaeb2bde8824b8c5894450fccdd9 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Tue, 9 Jun 2020 12:43:28 +0200 Subject: awful test --- src/python/gudhi/representations/vector_methods.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 98cd6153..77b2836f 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -615,16 +615,16 @@ class Atol(BaseEstimator, TransformerMixin): >>> c = np.array([[3, 2, -1], [1, 2, -1]]) >>> atol_vectoriser = Atol(quantiser=KMeans(n_clusters=2, random_state=202006)) >>> atol_vectoriser.fit(X=[a, b, c]).centers - array([[ 2.6 , 2.8 , -0.4 ], - [ 2. , 0.66666667, 3.33333333]]) + array([[ 2. , 0.66666667, 3.33333333], + [ 2.6 , 2.8 , -0.4 ]]) >>> atol_vectoriser(a) - array([0.58394704, 1.0769395 ]) + array([1.0769395 , 0.58394704]) >>> atol_vectoriser(c) - array([1.02816136, 0.23559623]) + array([0.23559623, 1.02816136]) >>> atol_vectoriser.transform(X=[a, b, c]) - array([[0.58394704, 1.0769395 ], - [1.04696684, 0.56203292], - [1.02816136, 0.23559623]]) + array([[1.0769395 , 0.58394704], + [0.56203292, 1.04696684], + [0.23559623, 1.02816136]]) """ def __init__(self, quantiser, weighting_method="cloud", contrast="gaussian"): """ -- cgit v1.2.3 From cdba6045ddf1dd41e8addb7351d1c87a5506ba0f Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:20:13 +0200 Subject: Apply suggestions from code review --- src/python/gudhi/representations/vector_methods.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 77b2836f..667f963b 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -578,17 +578,17 @@ class ComplexPolynomial(BaseEstimator, TransformerMixin): def _lapl_contrast(measure, centers, inertias, eps=1e-8): """contrast function for vectorising `measure` in ATOL""" - return np.exp(-np.sqrt(pairwise.pairwise_distances(measure, Y=centers) / (inertias + eps))) + return np.exp(-pairwise.pairwise_distances(measure, Y=centers) / (inertias + eps)) def _gaus_contrast(measure, centers, inertias, eps=1e-8): """contrast function for vectorising `measure` in ATOL""" - return np.exp(-pairwise.pairwise_distances(measure, Y=centers) / (inertias + eps)) + return np.exp(-pairwise.pairwise_distances(measure, Y=centers)**2 / (inertias**2 + eps)) def _indicator_contrast(diags, centers, inertias, eps=1e-8): """contrast function for vectorising `measure` in ATOL""" pair_dist = pairwise.pairwise_distances(diags, Y=centers) flat_circ = (pair_dist < (inertias+eps)).astype(int) - robe_curve = np.positive((2-pair_dist/(inertias+eps))*((inertias+eps) < pair_dist).astype(int)) + robe_curve = np.clip(2-pair_dist/(inertias+eps), 0, 1) return flat_circ + robe_curve def _cloud_weighting(measure): @@ -638,7 +638,7 @@ class Atol(BaseEstimator, TransformerMixin): (default: constant function, i.e. the measure is seen as a point cloud by default). This will have no impact if weights are provided along with measures all the way: `fit` and `transform`. contrast (string): constant function for evaluating proximity of a measure with respect to centers - choose from {"gaussian", "laplacian", "indicator"} + choose from {"gaussian", "laplacian", "indicator"} (default: gaussian contrast function, see page 3 in the ATOL paper). """ self.quantiser = quantiser @@ -670,7 +670,7 @@ class Atol(BaseEstimator, TransformerMixin): """ if not hasattr(self.quantiser, 'fit'): raise TypeError("quantiser %s has no `fit` attribute." % (self.quantiser)) - if len(X) < self.quantiser.n_clusters: + if np.sum([measure.shape[0] for measure in X]) < self.quantiser.n_clusters: # in case there are not enough observations for fitting the quantiser, we add random points in [0, 1]^2 # @Martin: perhaps this behaviour is to be externalised and a warning should be raised instead random_points = np.random.rand(self.quantiser.n_clusters-len(X), X[0].shape[1]) @@ -681,7 +681,6 @@ class Atol(BaseEstimator, TransformerMixin): measures_concat = np.concatenate(X) self.quantiser.fit(X=measures_concat, sample_weight=sample_weight) self.centers = self.quantiser.cluster_centers_ - labels = np.argmin(pairwise.pairwise_distances(measures_concat, Y=self.centers), axis=1) dist_centers = pairwise.pairwise_distances(self.centers) np.fill_diagonal(dist_centers, np.inf) self.inertias = np.min(dist_centers, axis=0)/2 -- cgit v1.2.3 From bef50e15e499e40d4dd4f5d991ec87eab4236108 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:32:48 +0200 Subject: remove epsilons --- src/python/gudhi/representations/vector_methods.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 667f963b..ede1087f 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -576,19 +576,19 @@ class ComplexPolynomial(BaseEstimator, TransformerMixin): """ return self.fit_transform([diag])[0,:] -def _lapl_contrast(measure, centers, inertias, eps=1e-8): +def _lapl_contrast(measure, centers, inertias): """contrast function for vectorising `measure` in ATOL""" - return np.exp(-pairwise.pairwise_distances(measure, Y=centers) / (inertias + eps)) + return np.exp(-pairwise.pairwise_distances(measure, Y=centers) / inertias) -def _gaus_contrast(measure, centers, inertias, eps=1e-8): +def _gaus_contrast(measure, centers, inertias): """contrast function for vectorising `measure` in ATOL""" - return np.exp(-pairwise.pairwise_distances(measure, Y=centers)**2 / (inertias**2 + eps)) + return np.exp(-pairwise.pairwise_distances(measure, Y=centers)**2 / inertias**2) -def _indicator_contrast(diags, centers, inertias, eps=1e-8): +def _indicator_contrast(diags, centers, inertias): """contrast function for vectorising `measure` in ATOL""" pair_dist = pairwise.pairwise_distances(diags, Y=centers) - flat_circ = (pair_dist < (inertias+eps)).astype(int) - robe_curve = np.clip(2-pair_dist/(inertias+eps), 0, 1) + flat_circ = (pair_dist < inertias).astype(int) + robe_curve = np.clip(2-pair_dist/inertias, 0, 1) return flat_circ + robe_curve def _cloud_weighting(measure): -- cgit v1.2.3 From 76529cae58f8a2736a1730fd81a9e12c3f4c7e19 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Thu, 11 Jun 2020 16:47:16 +0200 Subject: Apply suggestions from code review #456 (thank you Marc!) --- src/python/gudhi/representations/vector_methods.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index ede1087f..49c05c51 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -586,10 +586,8 @@ def _gaus_contrast(measure, centers, inertias): def _indicator_contrast(diags, centers, inertias): """contrast function for vectorising `measure` in ATOL""" - pair_dist = pairwise.pairwise_distances(diags, Y=centers) - flat_circ = (pair_dist < inertias).astype(int) - robe_curve = np.clip(2-pair_dist/inertias, 0, 1) - return flat_circ + robe_curve + robe_curve = np.clip(2-pairwise.pairwise_distances(diags, Y=centers)/inertias, 0, 1) + return robe_curve def _cloud_weighting(measure): """automatic uniform weighting with mass 1 for `measure` in ATOL""" @@ -603,7 +601,7 @@ class Atol(BaseEstimator, TransformerMixin): """ This class allows to vectorise measures (e.g. point clouds, persistence diagrams, etc) after a quantisation step. - ATOL paper: https://arxiv.org/abs/1909.13472 + ATOL paper: :cite:`royer2019atol` Example -------- @@ -632,9 +630,9 @@ class Atol(BaseEstimator, TransformerMixin): Parameters: quantiser (Object): Object with `fit` (sklearn API consistent) and `cluster_centers` and `n_clusters` - attributes. This object will be fitted by the function `fit`. + attributes, e.g. sklearn.cluster.KMeans. It will be fitted when the Atol object function `fit` is called. weighting_method (string): constant generic function for weighting the measure points - choose from {"cloud", "iidproba"} + choose from {"cloud", "iidproba"} (default: constant function, i.e. the measure is seen as a point cloud by default). This will have no impact if weights are provided along with measures all the way: `fit` and `transform`. contrast (string): constant function for evaluating proximity of a measure with respect to centers @@ -647,8 +645,6 @@ class Atol(BaseEstimator, TransformerMixin): "laplacian": _lapl_contrast, "indicator": _indicator_contrast, }.get(contrast, _gaus_contrast) - self.centers = np.ones(shape=(self.quantiser.n_clusters, 2))*np.inf - self.inertias = np.full(self.quantiser.n_clusters, np.nan) self.weighting_method = { "cloud" : _cloud_weighting, "iidproba": _iidproba_weighting, @@ -670,11 +666,6 @@ class Atol(BaseEstimator, TransformerMixin): """ if not hasattr(self.quantiser, 'fit'): raise TypeError("quantiser %s has no `fit` attribute." % (self.quantiser)) - if np.sum([measure.shape[0] for measure in X]) < self.quantiser.n_clusters: - # in case there are not enough observations for fitting the quantiser, we add random points in [0, 1]^2 - # @Martin: perhaps this behaviour is to be externalised and a warning should be raised instead - random_points = np.random.rand(self.quantiser.n_clusters-len(X), X[0].shape[1]) - X.append(random_points) if sample_weight is None: sample_weight = np.concatenate([self.weighting_method(measure) for measure in X]) -- cgit v1.2.3 From a90843c6bf5f7f05392c4262efb60e94ccfb0e48 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Thu, 11 Jun 2020 17:03:40 +0200 Subject: test value tweak --- src/python/gudhi/representations/vector_methods.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 49c05c51..5a45f179 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -616,13 +616,13 @@ class Atol(BaseEstimator, TransformerMixin): array([[ 2. , 0.66666667, 3.33333333], [ 2.6 , 2.8 , -0.4 ]]) >>> atol_vectoriser(a) - array([1.0769395 , 0.58394704]) + array([1.18168665, 0.42375966]) >>> atol_vectoriser(c) - array([0.23559623, 1.02816136]) + array([0.02062512, 1.25157463]) >>> atol_vectoriser.transform(X=[a, b, c]) - array([[1.0769395 , 0.58394704], - [0.56203292, 1.04696684], - [0.23559623, 1.02816136]]) + array([[1.18168665, 0.42375966], + [0.29861028, 1.06330156], + [0.02062512, 1.25157463]]) """ def __init__(self, quantiser, weighting_method="cloud", contrast="gaussian"): """ -- cgit v1.2.3 From ec1c3ad11aeb46a67926a615fd5c00fbc70b501e Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Thu, 11 Jun 2020 21:17:27 +0200 Subject: case n_centers = 1 --- src/python/gudhi/representations/vector_methods.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 5a45f179..a576267c 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -672,9 +672,14 @@ class Atol(BaseEstimator, TransformerMixin): measures_concat = np.concatenate(X) self.quantiser.fit(X=measures_concat, sample_weight=sample_weight) self.centers = self.quantiser.cluster_centers_ - dist_centers = pairwise.pairwise_distances(self.centers) - np.fill_diagonal(dist_centers, np.inf) - self.inertias = np.min(dist_centers, axis=0)/2 + if self.quantiser.n_clusters == 1: + dist_centers = pairwise.pairwise_distances(measures_concat) + np.fill_diagonal(dist_centers, 0) + self.inertias = np.max(dist_centers)/2 + else: + dist_centers = pairwise.pairwise_distances(self.centers) + np.fill_diagonal(dist_centers, np.inf) + self.inertias = np.min(dist_centers, axis=0)/2 return self def __call__(self, measure, sample_weight=None): -- cgit v1.2.3 From 1abc47f5bf65ee3451a907ecfc9db84c0471ef93 Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Tue, 16 Jun 2020 22:36:31 +0200 Subject: Update src/python/gudhi/representations/vector_methods.py --- src/python/gudhi/representations/vector_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index a576267c..566c24a3 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -675,7 +675,7 @@ class Atol(BaseEstimator, TransformerMixin): if self.quantiser.n_clusters == 1: dist_centers = pairwise.pairwise_distances(measures_concat) np.fill_diagonal(dist_centers, 0) - self.inertias = np.max(dist_centers)/2 + self.inertias = np.array([np.max(dist_centers)/2]) else: dist_centers = pairwise.pairwise_distances(self.centers) np.fill_diagonal(dist_centers, np.inf) -- cgit v1.2.3 From 4a558f9542283533d1218a35ce43751615ca2ffd Mon Sep 17 00:00:00 2001 From: martinroyer <16647869+martinroyer@users.noreply.github.com> Date: Fri, 19 Jun 2020 14:29:55 +0200 Subject: fix for null inertias --- src/python/gudhi/representations/vector_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 566c24a3..aaf7ffeb 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -678,7 +678,7 @@ class Atol(BaseEstimator, TransformerMixin): self.inertias = np.array([np.max(dist_centers)/2]) else: dist_centers = pairwise.pairwise_distances(self.centers) - np.fill_diagonal(dist_centers, np.inf) + dist_centers[dist_centers == 0] = np.inf self.inertias = np.min(dist_centers, axis=0)/2 return self -- cgit v1.2.3 From b1f656f0e9a29597e88df353d1684272023cba54 Mon Sep 17 00:00:00 2001 From: martinroyer-buntu Date: Fri, 3 Jul 2020 10:58:35 +0200 Subject: Author in representations_sum --- src/python/doc/representations_sum.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/doc/representations_sum.inc b/src/python/doc/representations_sum.inc index 323a0920..4298aea9 100644 --- a/src/python/doc/representations_sum.inc +++ b/src/python/doc/representations_sum.inc @@ -2,7 +2,7 @@ :widths: 30 40 30 +------------------------------------------------------------------+----------------------------------------------------------------+-------------------------------------------------------------+ - | .. figure:: | Vectorizations, distances and kernels that work on persistence | :Author: Mathieu Carrière | + | .. figure:: | Vectorizations, distances and kernels that work on persistence | :Author: Mathieu Carrière, Martin Royer | | img/sklearn-tda.png | diagrams, compatible with scikit-learn. | | | | | :Since: GUDHI 3.1.0 | | | | | -- cgit v1.2.3 From af49fdd761bf1eccb5fdca760a99e2e250895f64 Mon Sep 17 00:00:00 2001 From: martinroyer-buntu Date: Fri, 3 Jul 2020 10:58:54 +0200 Subject: dummy test for code coverage --- src/python/test/test_representations.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'src/python') diff --git a/src/python/test/test_representations.py b/src/python/test/test_representations.py index 589cee00..6a09be48 100755 --- a/src/python/test/test_representations.py +++ b/src/python/test/test_representations.py @@ -4,6 +4,8 @@ import matplotlib.pyplot as plt import numpy as np import pytest +from sklearn.cluster import KMeans + def test_representations_examples(): # Disable graphics for testing purposes @@ -15,6 +17,7 @@ def test_representations_examples(): return None +from gudhi.representations.vector_methods import Atol from gudhi.representations.metrics import * from gudhi.representations.kernel_methods import * @@ -41,3 +44,16 @@ def test_multiple(): d2 = WassersteinDistance(order=2, internal_p=2, n_jobs=4).fit(l2).transform(l1) print(d1.shape, d2.shape) assert d1 == pytest.approx(d2, rel=.02) + + +def test_dummy_atol(): + a = np.array([[1, 2, 4], [1, 4, 0], [1, 0, 4]]) + b = np.array([[4, 2, 0], [4, 4, 0], [4, 0, 2]]) + c = np.array([[3, 2, -1], [1, 2, -1]]) + + for weighting_method in ["cloud", "iidproba"]: + for contrast in ["gaussian", "laplacian", "indicator"]: + atol_vectoriser = Atol(quantiser=KMeans(n_clusters=1, random_state=202006), weighting_method=weighting_method, contrast=contrast) + atol_vectoriser(a) + atol_vectoriser.transform(X=[a, b, c]) + -- cgit v1.2.3 From b7d9cc2b1e8f58f563d23c3588d785ced98222b3 Mon Sep 17 00:00:00 2001 From: martinroyer-buntu Date: Fri, 3 Jul 2020 11:01:26 +0200 Subject: small optim --- src/python/gudhi/representations/vector_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index aaf7ffeb..5ca127f6 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -582,7 +582,7 @@ def _lapl_contrast(measure, centers, inertias): def _gaus_contrast(measure, centers, inertias): """contrast function for vectorising `measure` in ATOL""" - return np.exp(-pairwise.pairwise_distances(measure, Y=centers)**2 / inertias**2) + return np.exp(-pairwise.pairwise_distances(measure, Y=centers, squared=True) / inertias**2) def _indicator_contrast(diags, centers, inertias): """contrast function for vectorising `measure` in ATOL""" -- cgit v1.2.3 From 96eb09e4f034fd71f5674f75e5e4584a7402b218 Mon Sep 17 00:00:00 2001 From: martinroyer-buntu Date: Fri, 3 Jul 2020 13:43:58 +0200 Subject: missing fit in dummy test --- src/python/test/test_representations.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src/python') diff --git a/src/python/test/test_representations.py b/src/python/test/test_representations.py index 6a09be48..e5c211a0 100755 --- a/src/python/test/test_representations.py +++ b/src/python/test/test_representations.py @@ -54,6 +54,7 @@ def test_dummy_atol(): for weighting_method in ["cloud", "iidproba"]: for contrast in ["gaussian", "laplacian", "indicator"]: atol_vectoriser = Atol(quantiser=KMeans(n_clusters=1, random_state=202006), weighting_method=weighting_method, contrast=contrast) + atol_vectoriser.fit([a, b, c]) atol_vectoriser(a) atol_vectoriser.transform(X=[a, b, c]) -- cgit v1.2.3 From 2bf81f9caa70e30a0e3a8eaf792243a9d4a2436b Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 28 Jul 2020 11:55:02 +0200 Subject: Add mechanism to deactivate LaTeX matplotlib rendering --- .../doc/persistence_graphical_tools_user.rst | 14 +++++++ src/python/gudhi/persistence_graphical_tools.py | 46 ++++++++++++++-------- 2 files changed, 43 insertions(+), 17 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/persistence_graphical_tools_user.rst b/src/python/doc/persistence_graphical_tools_user.rst index b5a38eb1..5cdc2dca 100644 --- a/src/python/doc/persistence_graphical_tools_user.rst +++ b/src/python/doc/persistence_graphical_tools_user.rst @@ -90,3 +90,17 @@ If you want more information on a specific dimension, for instance: gudhi.plot_persistence_density(persistence=pers_diag, dimension=1, legend=True, axes=axes[1]) plt.show() + +LaTeX support +------------- + +By default, persistence graphical tools are using LaTeX support for matplotlib if available (cf. +`matplotlib text rendering with LaTeX `_). +It also requires `type1cm` LaTeX package (not detected by matplotlib). + +If you are facing issues with LaTeX rendering, you can still deactivate LaTeX rendering by saying: + +.. code-block:: python + + import gudhi + gudhi.persistence_graphical_tools._gudhi_matplotlib_use_tex=False diff --git a/src/python/gudhi/persistence_graphical_tools.py b/src/python/gudhi/persistence_graphical_tools.py index c6766c70..cce091d5 100644 --- a/src/python/gudhi/persistence_graphical_tools.py +++ b/src/python/gudhi/persistence_graphical_tools.py @@ -20,6 +20,20 @@ __author__ = "Vincent Rouvreau, Bertrand Michel, Theo Lacombe" __copyright__ = "Copyright (C) 2016 Inria" __license__ = "MIT" +@lru_cache(maxsize=1) +def _matplotlib_can_use_tex(): + """This function returns True if matplotlib can deal with LaTeX, False otherwise. + The returned value is cached. + """ + try: + from matplotlib import checkdep_usetex + return checkdep_usetex(True) + except ImportError: + print("This function is not available, you may be missing matplotlib.") + + +_gudhi_matplotlib_use_tex = _matplotlib_can_use_tex() + def __min_birth_max_death(persistence, band=0.0): """This function returns (min_birth, max_death) from the persistence. @@ -57,17 +71,6 @@ def _array_handler(a): else: return a -@lru_cache(maxsize=1) -def _matplotlib_can_use_tex(): - """This function returns True if matplotlib can deal with LaTeX, False otherwise. - The returned value is cached. - """ - try: - from matplotlib import checkdep_usetex - return checkdep_usetex(True) - except ImportError: - print("This function is not available, you may be missing matplotlib.") - def plot_persistence_barcode( persistence=[], @@ -117,10 +120,13 @@ def plot_persistence_barcode( try: import matplotlib.pyplot as plt import matplotlib.patches as mpatches - if _matplotlib_can_use_tex(): - from matplotlib import rc + from matplotlib import rc + if _gudhi_matplotlib_use_tex: plt.rc('text', usetex=True) plt.rc('font', family='serif') + else: + plt.rc('text', usetex=False) + plt.rc('font', family='DejaVu Sans') if persistence_file != "": if path.isfile(persistence_file): @@ -263,10 +269,13 @@ def plot_persistence_diagram( try: import matplotlib.pyplot as plt import matplotlib.patches as mpatches - if _matplotlib_can_use_tex(): - from matplotlib import rc + from matplotlib import rc + if _gudhi_matplotlib_use_tex: plt.rc('text', usetex=True) plt.rc('font', family='serif') + else: + plt.rc('text', usetex=False) + plt.rc('font', family='DejaVu Sans') if persistence_file != "": if path.isfile(persistence_file): @@ -436,10 +445,13 @@ def plot_persistence_density( import matplotlib.pyplot as plt import matplotlib.patches as mpatches from scipy.stats import kde - if _matplotlib_can_use_tex(): - from matplotlib import rc + from matplotlib import rc + if _gudhi_matplotlib_use_tex: plt.rc('text', usetex=True) plt.rc('font', family='serif') + else: + plt.rc('text', usetex=False) + plt.rc('font', family='DejaVu Sans') if persistence_file != "": if dimension is None: -- cgit v1.2.3 From 37f806271cc5f00525cbabe0f2ec9db440d455ee Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 28 Jul 2020 14:59:13 +0200 Subject: Code review: Do not print warnings on import gudhi --- src/python/gudhi/persistence_graphical_tools.py | 32 +++++++++++-------------- 1 file changed, 14 insertions(+), 18 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/persistence_graphical_tools.py b/src/python/gudhi/persistence_graphical_tools.py index cce091d5..c9c85e46 100644 --- a/src/python/gudhi/persistence_graphical_tools.py +++ b/src/python/gudhi/persistence_graphical_tools.py @@ -11,7 +11,6 @@ from os import path from math import isfinite import numpy as np -from functools import lru_cache from gudhi.reader_utils import read_persistence_intervals_in_dimension from gudhi.reader_utils import read_persistence_intervals_grouped_by_dimension @@ -20,20 +19,7 @@ __author__ = "Vincent Rouvreau, Bertrand Michel, Theo Lacombe" __copyright__ = "Copyright (C) 2016 Inria" __license__ = "MIT" -@lru_cache(maxsize=1) -def _matplotlib_can_use_tex(): - """This function returns True if matplotlib can deal with LaTeX, False otherwise. - The returned value is cached. - """ - try: - from matplotlib import checkdep_usetex - return checkdep_usetex(True) - except ImportError: - print("This function is not available, you may be missing matplotlib.") - - -_gudhi_matplotlib_use_tex = _matplotlib_can_use_tex() - +_gudhi_matplotlib_use_tex = True def __min_birth_max_death(persistence, band=0.0): """This function returns (min_birth, max_death) from the persistence. @@ -71,6 +57,16 @@ def _array_handler(a): else: return a +def _matplotlib_can_use_tex(): + """This function returns True if matplotlib can deal with LaTeX, False otherwise. + The returned value is cached. + """ + try: + from matplotlib import checkdep_usetex + return checkdep_usetex(True) + except ImportError: + print("This function is not available, you may be missing matplotlib.") + def plot_persistence_barcode( persistence=[], @@ -121,7 +117,7 @@ def plot_persistence_barcode( import matplotlib.pyplot as plt import matplotlib.patches as mpatches from matplotlib import rc - if _gudhi_matplotlib_use_tex: + if _gudhi_matplotlib_use_tex and _matplotlib_can_use_tex(): plt.rc('text', usetex=True) plt.rc('font', family='serif') else: @@ -270,7 +266,7 @@ def plot_persistence_diagram( import matplotlib.pyplot as plt import matplotlib.patches as mpatches from matplotlib import rc - if _gudhi_matplotlib_use_tex: + if _gudhi_matplotlib_use_tex and _matplotlib_can_use_tex(): plt.rc('text', usetex=True) plt.rc('font', family='serif') else: @@ -446,7 +442,7 @@ def plot_persistence_density( import matplotlib.patches as mpatches from scipy.stats import kde from matplotlib import rc - if _gudhi_matplotlib_use_tex: + if _gudhi_matplotlib_use_tex and _matplotlib_can_use_tex(): plt.rc('text', usetex=True) plt.rc('font', family='serif') else: -- cgit v1.2.3 From d9d4bf2ef2c2676389cca4ce4e6595ca2e4ca0b1 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 28 Jul 2020 16:31:05 +0200 Subject: Code review: rollback lru_cache --- src/python/gudhi/persistence_graphical_tools.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/python') diff --git a/src/python/gudhi/persistence_graphical_tools.py b/src/python/gudhi/persistence_graphical_tools.py index c9c85e46..848dc03e 100644 --- a/src/python/gudhi/persistence_graphical_tools.py +++ b/src/python/gudhi/persistence_graphical_tools.py @@ -11,6 +11,7 @@ from os import path from math import isfinite import numpy as np +from functools import lru_cache from gudhi.reader_utils import read_persistence_intervals_in_dimension from gudhi.reader_utils import read_persistence_intervals_grouped_by_dimension @@ -57,6 +58,7 @@ def _array_handler(a): else: return a +@lru_cache(maxsize=1) def _matplotlib_can_use_tex(): """This function returns True if matplotlib can deal with LaTeX, False otherwise. The returned value is cached. -- cgit v1.2.3 From 76a61bcd3279a98bd84856b011869a0be2ba99cd Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 30 Jul 2020 12:36:16 +0200 Subject: collapse edges for python simplex tree --- .../example/rips_complex_edge_collapse_example.py | 65 ++++++++++++++++++++++ src/python/gudhi/simplex_tree.pxd | 1 + src/python/gudhi/simplex_tree.pyx | 63 +++++++++++++-------- src/python/include/Simplex_tree_interface.h | 29 ++++++++++ src/python/test/test_simplex_tree.py | 16 ++++++ 5 files changed, 151 insertions(+), 23 deletions(-) create mode 100755 src/python/example/rips_complex_edge_collapse_example.py (limited to 'src/python') diff --git a/src/python/example/rips_complex_edge_collapse_example.py b/src/python/example/rips_complex_edge_collapse_example.py new file mode 100755 index 00000000..e352c155 --- /dev/null +++ b/src/python/example/rips_complex_edge_collapse_example.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +import gudhi +import matplotlib.pyplot as plt +import time + +""" 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) 2016 Inria + + Modification(s): + - YYYY/MM Author: Description of the modification +""" + +__author__ = "Vincent Rouvreau" +__copyright__ = "Copyright (C) 2020 Inria" +__license__ = "MIT" + + +print("#####################################################################") +print("RipsComplex (only the one-skeleton) creation from tore3D_300.off file") + +off_file = gudhi.__root_source_dir__ + '/data/points/tore3D_300.off' +point_cloud = gudhi.read_points_from_off_file(off_file = off_file) +rips_complex = gudhi.RipsComplex(points=point_cloud, max_edge_length=12.0) +simplex_tree = rips_complex.create_simplex_tree(max_dimension=1) +result_str = '1. Rips complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ + repr(simplex_tree.num_simplices()) + ' simplices - ' + \ + repr(simplex_tree.num_vertices()) + ' vertices.' +print(result_str) + +# Expansion of this one-skeleton would require a lot of memory. Let's collapse it +start = time.process_time() +simplex_tree.collapse_edges() +simplex_tree.expansion(3) +diag = simplex_tree.persistence() +print("Collapse, expansion and persistence computation took ", time.process_time() - start, " sec.") +result_str = '2. Rips complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ + repr(simplex_tree.num_simplices()) + ' simplices - ' + \ + repr(simplex_tree.num_vertices()) + ' vertices.' +print(result_str) + +# Use subplots to display diagram and density side by side +fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 5)) +gudhi.plot_persistence_diagram(diag, axes=axes[0]) +axes[0].set_title("Persistence after 1 collapse") + +# Collapse can be performed several times. Let's collapse it 3 times +start = time.process_time() +simplex_tree.collapse_edges(nb_iterations = 3) +simplex_tree.expansion(3) +diag = simplex_tree.persistence() +print("Collapse, expansion and persistence computation took ", time.process_time() - start, " sec.") +result_str = '3. Rips complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ + repr(simplex_tree.num_simplices()) + ' simplices - ' + \ + repr(simplex_tree.num_vertices()) + ' vertices.' +print(result_str) + +gudhi.plot_persistence_diagram(diag, axes=axes[1]) +axes[1].set_title("Persistence after 3 more collapses") + +# Plot the 2 persistence diagrams side to side to check the persistence is the same +plt.show() diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index e748ac40..75e94e0b 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -57,6 +57,7 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": bool make_filtration_non_decreasing() nogil void compute_extended_filtration() nogil vector[vector[pair[int, pair[double, double]]]] compute_extended_persistence_subdiagrams(vector[pair[int, pair[double, double]]] dgm, double min_persistence) nogil + Simplex_tree_interface_full_featured* collapse_edges(int nb_collapse_iteration) nogil # Iterators over Simplex tree pair[vector[int], double] get_simplex_and_filtration(Simplex_tree_simplex_handle f_simplex) nogil Simplex_tree_simplices_iterator get_simplices_iterator_begin() nogil diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 20e66d9f..236435a7 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -69,7 +69,7 @@ cdef class SimplexTree: this simplicial complex, or +infinity if it is not in the complex. :param simplex: The N-simplex, represented by a list of vertex. - :type simplex: list of int. + :type simplex: list of int :returns: The simplicial complex filtration value. :rtype: float """ @@ -80,7 +80,7 @@ cdef class SimplexTree: given N-simplex. :param simplex: The N-simplex, represented by a list of vertex. - :type simplex: list of int. + :type simplex: list of int :param filtration: The new filtration value. :type filtration: float @@ -153,7 +153,7 @@ cdef class SimplexTree: """This function sets the dimension of the simplicial complex. :param dimension: The new dimension value. - :type dimension: int. + :type dimension: int .. note:: @@ -172,7 +172,7 @@ cdef class SimplexTree: complex or not. :param simplex: The N-simplex to find, represented by a list of vertex. - :type simplex: list of int. + :type simplex: list of int :returns: true if the simplex was found, false otherwise. :rtype: bool """ @@ -186,9 +186,9 @@ cdef class SimplexTree: :param simplex: The N-simplex to insert, represented by a list of vertex. - :type simplex: list of int. + :type simplex: list of int :param filtration: The filtration value of the simplex. - :type filtration: float. + :type filtration: float :returns: true if the simplex was not yet in the complex, false otherwise (whatever its original filtration value). :rtype: bool @@ -228,7 +228,7 @@ cdef class SimplexTree: """This function returns a generator with the (simplices of the) skeleton of a maximum given dimension. :param dimension: The skeleton dimension value. - :type dimension: int. + :type dimension: int :returns: The (simplices of the) skeleton of a maximum dimension. :rtype: generator with tuples(simplex, filtration) """ @@ -243,7 +243,7 @@ cdef class SimplexTree: """This function returns the star of a given N-simplex. :param simplex: The N-simplex, represented by a list of vertex. - :type simplex: list of int. + :type simplex: list of int :returns: The (simplices of the) star of a simplex. :rtype: list of tuples(simplex, filtration) """ @@ -265,10 +265,10 @@ cdef class SimplexTree: given codimension. :param simplex: The N-simplex, represented by a list of vertex. - :type simplex: list of int. + :type simplex: list of int :param codimension: The codimension. If codimension = 0, all cofaces are returned (equivalent of get_star function) - :type codimension: int. + :type codimension: int :returns: The (simplices of the) cofaces of a simplex :rtype: list of tuples(simplex, filtration) """ @@ -290,7 +290,7 @@ cdef class SimplexTree: complex. :param simplex: The N-simplex, represented by a list of vertex. - :type simplex: list of int. + :type simplex: list of int .. note:: @@ -308,7 +308,7 @@ cdef class SimplexTree: """Prune above filtration value given as parameter. :param filtration: Maximum threshold value. - :type filtration: float. + :type filtration: float :returns: The filtration modification information. :rtype: bool @@ -342,7 +342,7 @@ cdef class SimplexTree: 1 when calling the method. :param max_dim: The maximal dimension. - :type max_dim: int. + :type max_dim: int """ cdef int maxdim = max_dim with nogil: @@ -383,12 +383,12 @@ cdef class SimplexTree: :param homology_coeff_field: The homology coefficient field. Must be a prime number. Default value is 11. - :type homology_coeff_field: int. + :type homology_coeff_field: int :param min_persistence: The minimum persistence value (i.e., the absolute value of the difference between the persistence diagram point coordinates) to take into account (strictly greater than min_persistence). Default value is 0.0. Sets min_persistence to -1.0 to see all values. - :type min_persistence: float. + :type min_persistence: float :returns: A list of four persistence diagrams in the format described in :func:`persistence`. The first one is Ordinary, the second one is Relative, the third one is Extended+ and the fourth one is Extended-. See https://link.springer.com/article/10.1007/s10208-008-9027-z and/or section 2.2 in https://link.springer.com/article/10.1007/s10208-017-9370-z for a description of these subtypes. .. note:: @@ -415,12 +415,12 @@ cdef class SimplexTree: :param homology_coeff_field: The homology coefficient field. Must be a prime number. Default value is 11. - :type homology_coeff_field: int. + :type homology_coeff_field: int :param min_persistence: The minimum persistence value to take into account (strictly greater than min_persistence). Default value is 0.0. Set min_persistence to -1.0 to see all values. - :type min_persistence: float. + :type min_persistence: float :param persistence_dim_max: If true, the persistent homology for the maximal dimension in the complex is computed. If false, it is ignored. Default is false. @@ -438,12 +438,12 @@ cdef class SimplexTree: :param homology_coeff_field: The homology coefficient field. Must be a prime number. Default value is 11. - :type homology_coeff_field: int. + :type homology_coeff_field: int :param min_persistence: The minimum persistence value to take into account (strictly greater than min_persistence). Default value is 0.0. Sets min_persistence to -1.0 to see all values. - :type min_persistence: float. + :type min_persistence: float :param persistence_dim_max: If true, the persistent homology for the maximal dimension in the complex is computed. If false, it is ignored. Default is false. @@ -478,10 +478,10 @@ cdef class SimplexTree: :param from_value: The persistence birth limit to be added in the numbers (persistent birth <= from_value). - :type from_value: float. + :type from_value: float :param to_value: The persistence death limit to be added in the numbers (persistent death > to_value). - :type to_value: float. + :type to_value: float :returns: The persistent Betti numbers ([B0, B1, ..., Bn]). :rtype: list of int @@ -498,7 +498,7 @@ cdef class SimplexTree: complex in a specific dimension. :param dimension: The specific dimension. - :type dimension: int. + :type dimension: int :returns: The persistence intervals. :rtype: numpy array of dimension 2 @@ -527,7 +527,7 @@ cdef class SimplexTree: complex in a user given file name. :param persistence_file: Name of the file. - :type persistence_file: string. + :type persistence_file: string :note: intervals_in_dim function requires :func:`compute_persistence` @@ -581,3 +581,20 @@ cdef class SimplexTree: infinite0 = np_array(next(l)) infinites = [np_array(d).reshape(-1,2) for d in l] return (normal0, normals, infinite0, infinites) + + def collapse_edges(self, nb_iterations = 1): + """Assuming the simplex tree is a 1-skeleton graph, this function collapse edges and resets the simplex tree + from the remaining edges. + A good candidate is to build a simplex tree on top of a :class:`~gudhi.RipsComplex` of dimension 1 before + collapsing edges. + + :param nb_iterations: The number of edge collapse iterations to perform. Default is 1. + :type nb_iterations: int + """ + # Backup old pointer + cdef Simplex_tree_interface_full_featured* ptr = self.get_ptr() + # New pointer is a new collapsed simplex tree + self.thisptr = (self.get_ptr().collapse_edges(nb_iterations)) + # Delete old pointer + if ptr != NULL: + del ptr diff --git a/src/python/include/Simplex_tree_interface.h b/src/python/include/Simplex_tree_interface.h index 56d7c41d..7500098d 100644 --- a/src/python/include/Simplex_tree_interface.h +++ b/src/python/include/Simplex_tree_interface.h @@ -15,10 +15,12 @@ #include #include #include +#include #include #include #include // std::pair +#include namespace Gudhi { @@ -157,6 +159,33 @@ class Simplex_tree_interface : public Simplex_tree { return new_dgm; } + Simplex_tree_interface* collapse_edges(int nb_collapse_iteration) { + using Filtered_edge = std::tuple; + std::vector edges; + for (Simplex_handle sh : Base::skeleton_simplex_range(1)) { + if (Base::dimension(sh) == 1) { + typename Base::Simplex_vertex_range rg = Base::simplex_vertex_range(sh); + std::vector rips_edge(rg.begin(), rg.end()); + edges.push_back(std::make_tuple(rips_edge[0], rips_edge[1], Base::filtration(sh))); + } + } + + std::vector remaining_edges; + for (int iteration = 0; iteration < nb_collapse_iteration; iteration++) { + remaining_edges = Gudhi::collapse::flag_complex_collapse_edges(edges); + edges = std::move(remaining_edges); + remaining_edges.clear(); + } + Simplex_tree_interface* collapsed_stree_ptr = new Simplex_tree_interface(); + for (auto remaining_edge : edges) { + collapsed_stree_ptr->insert({std::get<0>(remaining_edge)}, 0.); + collapsed_stree_ptr->insert({std::get<1>(remaining_edge)}, 0.); + collapsed_stree_ptr->insert({std::get<0>(remaining_edge), std::get<1>(remaining_edge)}, std::get<2>(remaining_edge)); + } + collapsed_stree_ptr->initialize_filtration(); + return collapsed_stree_ptr; + } + // Iterator over the simplex tree Complex_simplex_iterator get_simplices_iterator_begin() { // this specific case works because the range is just a pair of iterators - won't work if range was a vector diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index 2137d822..30a8f5e0 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -340,3 +340,19 @@ def test_simplices_iterator(): assert st.find(simplex[0]) == True print("filtration is: ", simplex[1]) assert st.filtration(simplex[0]) == simplex[1] + +def test_collapse_edges(): + st = SimplexTree() + + assert st.insert([0, 1], filtration=1.0) == True + assert st.insert([1, 2], filtration=1.0) == True + assert st.insert([2, 3], filtration=1.0) == True + assert st.insert([0, 3], filtration=1.0) == True + assert st.insert([0, 2], filtration=2.0) == True + assert st.insert([1, 3], filtration=2.0) == True + + assert st.num_simplices() == 10 + + st.collapse_edges() + assert st.num_simplices() == 9 + assert st.find([1, 3]) == False -- cgit v1.2.3 From bbff1a3ae9a8c560ac3a84e35b6b2e949072fa60 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 31 Jul 2020 09:27:55 +0200 Subject: New website header for pip download --- src/common/doc/header.html | 115 +++++++++++++++++----------------- src/python/doc/_templates/layout.html | 53 ++++++++-------- 2 files changed, 85 insertions(+), 83 deletions(-) (limited to 'src/python') diff --git a/src/common/doc/header.html b/src/common/doc/header.html index 99ab6bb7..9da20bbc 100644 --- a/src/common/doc/header.html +++ b/src/common/doc/header.html @@ -24,64 +24,65 @@ $extrastylesheet - + + + + + - + + {%- block header %}{% endblock %} -- cgit v1.2.3 From cb79887df3f5155224ccc6c79d6a0e04853925ea Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 31 Jul 2020 14:59:22 +0200 Subject: code review: simplifify traces --- .../example/rips_complex_edge_collapse_example.py | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) (limited to 'src/python') diff --git a/src/python/example/rips_complex_edge_collapse_example.py b/src/python/example/rips_complex_edge_collapse_example.py index e352c155..b26eb9fc 100755 --- a/src/python/example/rips_complex_edge_collapse_example.py +++ b/src/python/example/rips_complex_edge_collapse_example.py @@ -26,21 +26,19 @@ off_file = gudhi.__root_source_dir__ + '/data/points/tore3D_300.off' point_cloud = gudhi.read_points_from_off_file(off_file = off_file) rips_complex = gudhi.RipsComplex(points=point_cloud, max_edge_length=12.0) simplex_tree = rips_complex.create_simplex_tree(max_dimension=1) -result_str = '1. Rips complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ - repr(simplex_tree.num_simplices()) + ' simplices - ' + \ - repr(simplex_tree.num_vertices()) + ' vertices.' -print(result_str) +print('1. Rips complex is of dimension ', simplex_tree.dimension(), ' - ', + simplex_tree.num_simplices(), ' simplices - ', + simplex_tree.num_vertices(), ' vertices.') # Expansion of this one-skeleton would require a lot of memory. Let's collapse it start = time.process_time() simplex_tree.collapse_edges() +print('2. Rips complex is of dimension ', simplex_tree.dimension(), ' - ', + simplex_tree.num_simplices(), ' simplices - ', + simplex_tree.num_vertices(), ' vertices.') simplex_tree.expansion(3) diag = simplex_tree.persistence() print("Collapse, expansion and persistence computation took ", time.process_time() - start, " sec.") -result_str = '2. Rips complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ - repr(simplex_tree.num_simplices()) + ' simplices - ' + \ - repr(simplex_tree.num_vertices()) + ' vertices.' -print(result_str) # Use subplots to display diagram and density side by side fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 5)) @@ -50,16 +48,15 @@ axes[0].set_title("Persistence after 1 collapse") # Collapse can be performed several times. Let's collapse it 3 times start = time.process_time() simplex_tree.collapse_edges(nb_iterations = 3) +print('3. Rips complex is of dimension ', simplex_tree.dimension(), ' - ', + simplex_tree.num_simplices(), ' simplices - ', + simplex_tree.num_vertices(), ' vertices.') simplex_tree.expansion(3) diag = simplex_tree.persistence() print("Collapse, expansion and persistence computation took ", time.process_time() - start, " sec.") -result_str = '3. Rips complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ - repr(simplex_tree.num_simplices()) + ' simplices - ' + \ - repr(simplex_tree.num_vertices()) + ' vertices.' -print(result_str) gudhi.plot_persistence_diagram(diag, axes=axes[1]) axes[1].set_title("Persistence after 3 more collapses") # Plot the 2 persistence diagrams side to side to check the persistence is the same -plt.show() +plt.show() \ No newline at end of file -- cgit v1.2.3 From 12c0266d7defec73a6e8da3990a7fc8d482cbde5 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 31 Jul 2020 15:23:40 +0200 Subject: code review: nogil and use ptr as suggested --- src/python/gudhi/simplex_tree.pyx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 236435a7..9eee5e14 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -593,8 +593,10 @@ cdef class SimplexTree: """ # Backup old pointer cdef Simplex_tree_interface_full_featured* ptr = self.get_ptr() - # New pointer is a new collapsed simplex tree - self.thisptr = (self.get_ptr().collapse_edges(nb_iterations)) - # Delete old pointer - if ptr != NULL: - del ptr + cdef int nb_iter = nb_iterations + with nogil: + # New pointer is a new collapsed simplex tree + self.thisptr = (ptr.collapse_edges(nb_iter)) + # Delete old pointer + if ptr != NULL: + del ptr -- cgit v1.2.3 From d27d7839a3c32513ffc60eb709765c6a89e0e208 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 31 Jul 2020 16:21:55 +0200 Subject: Rebuild example page and link to example. Add also a link to the publication. Doc review: document edge collapse ignores simplices of higher dimension. --- src/python/doc/examples.rst | 33 ++++++++++++++++++--------------- src/python/gudhi/simplex_tree.pyx | 8 +++++--- 2 files changed, 23 insertions(+), 18 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/examples.rst b/src/python/doc/examples.rst index a42227e3..76e5d4c7 100644 --- a/src/python/doc/examples.rst +++ b/src/python/doc/examples.rst @@ -7,27 +7,30 @@ Examples .. only:: builder_html - * :download:`rips_complex_from_points_example.py <../example/rips_complex_from_points_example.py>` + * :download:`alpha_complex_diagram_persistence_from_off_file_example.py <../example/alpha_complex_diagram_persistence_from_off_file_example.py>` * :download:`alpha_complex_from_points_example.py <../example/alpha_complex_from_points_example.py>` - * :download:`simplex_tree_example.py <../example/simplex_tree_example.py>` * :download:`alpha_rips_persistence_bottleneck_distance.py <../example/alpha_rips_persistence_bottleneck_distance.py>` - * :download:`tangential_complex_plain_homology_from_off_file_example.py <../example/tangential_complex_plain_homology_from_off_file_example.py>` - * :download:`alpha_complex_diagram_persistence_from_off_file_example.py <../example/alpha_complex_diagram_persistence_from_off_file_example.py>` - * :download:`periodic_cubical_complex_barcode_persistence_from_perseus_file_example.py <../example/periodic_cubical_complex_barcode_persistence_from_perseus_file_example.py>` * :download:`bottleneck_basic_example.py <../example/bottleneck_basic_example.py>` - * :download:`gudhi_graphical_tools_example.py <../example/gudhi_graphical_tools_example.py>` - * :download:`plot_simplex_tree_dim012.py <../example/plot_simplex_tree_dim012.py>` - * :download:`plot_rips_complex.py <../example/plot_rips_complex.py>` - * :download:`plot_alpha_complex.py <../example/plot_alpha_complex.py>` - * :download:`witness_complex_from_nearest_landmark_table.py <../example/witness_complex_from_nearest_landmark_table.py>` + * :download:`coordinate_graph_induced_complex.py <../example/coordinate_graph_induced_complex.py>` + * :download:`diagram_vectorizations_distances_kernels.py <../example/diagram_vectorizations_distances_kernels.py>` * :download:`euclidean_strong_witness_complex_diagram_persistence_from_off_file_example.py <../example/euclidean_strong_witness_complex_diagram_persistence_from_off_file_example.py>` * :download:`euclidean_witness_complex_diagram_persistence_from_off_file_example.py <../example/euclidean_witness_complex_diagram_persistence_from_off_file_example.py>` - * :download:`rips_complex_diagram_persistence_from_off_file_example.py <../example/rips_complex_diagram_persistence_from_off_file_example.py>` + * :download:`functional_graph_induced_complex.py <../example/functional_graph_induced_complex.py>` + * :download:`gudhi_graphical_tools_example.py <../example/gudhi_graphical_tools_example.py>` + * :download:`nerve_of_a_covering.py <../example/nerve_of_a_covering.py>` + * :download:`periodic_cubical_complex_barcode_persistence_from_perseus_file_example.py <../example/periodic_cubical_complex_barcode_persistence_from_perseus_file_example.py>` + * :download:`plot_alpha_complex.py <../example/plot_alpha_complex.py>` + * :download:`plot_rips_complex.py <../example/plot_rips_complex.py>` + * :download:`plot_simplex_tree_dim012.py <../example/plot_simplex_tree_dim012.py>` + * :download:`random_cubical_complex_persistence_example.py <../example/random_cubical_complex_persistence_example.py>` + * :download:`rips_complex_diagram_persistence_from_correlation_matrix_file_example.py <../example/rips_complex_diagram_persistence_from_correlation_matrix_file_example.py>` * :download:`rips_complex_diagram_persistence_from_distance_matrix_file_example.py <../example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py>` + * :download:`rips_complex_diagram_persistence_from_off_file_example.py <../example/rips_complex_diagram_persistence_from_off_file_example.py>` + * :download:`rips_complex_edge_collapse_example.py <../example/rips_complex_edge_collapse_example.py>` + * :download:`rips_complex_from_points_example.py <../example/rips_complex_from_points_example.py>` * :download:`rips_persistence_diagram.py <../example/rips_persistence_diagram.py>` + * :download:`simplex_tree_example.py <../example/simplex_tree_example.py>` * :download:`sparse_rips_persistence_diagram.py <../example/sparse_rips_persistence_diagram.py>` - * :download:`random_cubical_complex_persistence_example.py <../example/random_cubical_complex_persistence_example.py>` - * :download:`coordinate_graph_induced_complex.py <../example/coordinate_graph_induced_complex.py>` - * :download:`functional_graph_induced_complex.py <../example/functional_graph_induced_complex.py>` + * :download:`tangential_complex_plain_homology_from_off_file_example.py <../example/tangential_complex_plain_homology_from_off_file_example.py>` * :download:`voronoi_graph_induced_complex.py <../example/voronoi_graph_induced_complex.py>` - * :download:`nerve_of_a_covering.py <../example/nerve_of_a_covering.py>` + * :download:`witness_complex_from_nearest_landmark_table.py <../example/witness_complex_from_nearest_landmark_table.py>` diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 9eee5e14..62f81d0b 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -583,10 +583,12 @@ cdef class SimplexTree: return (normal0, normals, infinite0, infinites) def collapse_edges(self, nb_iterations = 1): - """Assuming the simplex tree is a 1-skeleton graph, this function collapse edges and resets the simplex tree - from the remaining edges. + """Assuming the simplex tree is a 1-skeleton graph, this method collapse edges (simplices of higher dimension + are ignored) and resets the simplex tree from the remaining edges. A good candidate is to build a simplex tree on top of a :class:`~gudhi.RipsComplex` of dimension 1 before - collapsing edges. + collapsing edges + (cf. :download:`rips_complex_edge_collapse_example.py <../example/rips_complex_edge_collapse_example.py>`). + For implementation details, please refer to :cite:`edgecollapsesocg2020`. :param nb_iterations: The number of edge collapse iterations to perform. Default is 1. :type nb_iterations: int -- cgit v1.2.3 From 70196802c49104e27617e5bff1e3c6afd09271ea Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 31 Jul 2020 17:09:36 +0200 Subject: code review: using a vector is overkill. emplace_back is more efficient --- src/python/include/Simplex_tree_interface.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'src/python') diff --git a/src/python/include/Simplex_tree_interface.h b/src/python/include/Simplex_tree_interface.h index 7500098d..45b178a2 100644 --- a/src/python/include/Simplex_tree_interface.h +++ b/src/python/include/Simplex_tree_interface.h @@ -21,6 +21,7 @@ #include #include // std::pair #include +#include // for std::distance namespace Gudhi { @@ -163,10 +164,11 @@ class Simplex_tree_interface : public Simplex_tree { using Filtered_edge = std::tuple; std::vector edges; for (Simplex_handle sh : Base::skeleton_simplex_range(1)) { - if (Base::dimension(sh) == 1) { - typename Base::Simplex_vertex_range rg = Base::simplex_vertex_range(sh); - std::vector rips_edge(rg.begin(), rg.end()); - edges.push_back(std::make_tuple(rips_edge[0], rips_edge[1], Base::filtration(sh))); + typename Base::Simplex_vertex_range rg = Base::simplex_vertex_range(sh); + auto rg_begin = rg.begin(); + // We take only edges into account + if (std::distance(rg_begin, rg.end()) == 2) { + edges.emplace_back(*rg_begin, *std::next(rg_begin), Base::filtration(sh)); } } -- cgit v1.2.3 From 3a05c55a04a73b6e61d49506ef72a260ce6cd436 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 31 Jul 2020 17:15:54 +0200 Subject: code review: remove initialize_filtration --- src/python/include/Simplex_tree_interface.h | 1 - 1 file changed, 1 deletion(-) (limited to 'src/python') diff --git a/src/python/include/Simplex_tree_interface.h b/src/python/include/Simplex_tree_interface.h index 45b178a2..959257fa 100644 --- a/src/python/include/Simplex_tree_interface.h +++ b/src/python/include/Simplex_tree_interface.h @@ -184,7 +184,6 @@ class Simplex_tree_interface : public Simplex_tree { collapsed_stree_ptr->insert({std::get<1>(remaining_edge)}, 0.); collapsed_stree_ptr->insert({std::get<0>(remaining_edge), std::get<1>(remaining_edge)}, std::get<2>(remaining_edge)); } - collapsed_stree_ptr->initialize_filtration(); return collapsed_stree_ptr; } -- cgit v1.2.3 From c9b743e5fed3f33b2084d5d8add2b7db2504004b Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 31 Jul 2020 17:19:30 +0200 Subject: code review: no need to use any other temporary vector --- src/python/include/Simplex_tree_interface.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src/python') diff --git a/src/python/include/Simplex_tree_interface.h b/src/python/include/Simplex_tree_interface.h index 959257fa..ad0f9a28 100644 --- a/src/python/include/Simplex_tree_interface.h +++ b/src/python/include/Simplex_tree_interface.h @@ -172,11 +172,8 @@ class Simplex_tree_interface : public Simplex_tree { } } - std::vector remaining_edges; for (int iteration = 0; iteration < nb_collapse_iteration; iteration++) { - remaining_edges = Gudhi::collapse::flag_complex_collapse_edges(edges); - edges = std::move(remaining_edges); - remaining_edges.clear(); + edges = Gudhi::collapse::flag_complex_collapse_edges(edges); } Simplex_tree_interface* collapsed_stree_ptr = new Simplex_tree_interface(); for (auto remaining_edge : edges) { -- cgit v1.2.3 From 39fba06ef758483bc237b9375413974c3bbc16e4 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 31 Jul 2020 17:34:47 +0200 Subject: code review: collapse edges should copy the 0-skeleton. A test was added --- src/python/include/Simplex_tree_interface.h | 7 +++++-- src/python/test/test_simplex_tree.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) (limited to 'src/python') diff --git a/src/python/include/Simplex_tree_interface.h b/src/python/include/Simplex_tree_interface.h index ad0f9a28..f786ad6e 100644 --- a/src/python/include/Simplex_tree_interface.h +++ b/src/python/include/Simplex_tree_interface.h @@ -176,9 +176,12 @@ class Simplex_tree_interface : public Simplex_tree { edges = Gudhi::collapse::flag_complex_collapse_edges(edges); } Simplex_tree_interface* collapsed_stree_ptr = new Simplex_tree_interface(); + // Copy the original 0-skeleton + for (Simplex_handle sh : Base::skeleton_simplex_range(0)) { + collapsed_stree_ptr->insert({*(Base::simplex_vertex_range(sh).begin())}, Base::filtration(sh)); + } + // Insert remaining edges for (auto remaining_edge : edges) { - collapsed_stree_ptr->insert({std::get<0>(remaining_edge)}, 0.); - collapsed_stree_ptr->insert({std::get<1>(remaining_edge)}, 0.); collapsed_stree_ptr->insert({std::get<0>(remaining_edge), std::get<1>(remaining_edge)}, std::get<2>(remaining_edge)); } return collapsed_stree_ptr; diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index 30a8f5e0..83be0602 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -356,3 +356,5 @@ def test_collapse_edges(): st.collapse_edges() assert st.num_simplices() == 9 assert st.find([1, 3]) == False + for simplex in st.get_skeleton(0): + assert simplex[1] == 1. -- cgit v1.2.3 From c3a4fc7bdcc69a5d56761c67a77c7e5b6ff6d1ee Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Mon, 3 Aug 2020 08:32:48 +0200 Subject: code review: simplify edge parsing --- src/python/include/Simplex_tree_interface.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src/python') diff --git a/src/python/include/Simplex_tree_interface.h b/src/python/include/Simplex_tree_interface.h index f786ad6e..e288a8cf 100644 --- a/src/python/include/Simplex_tree_interface.h +++ b/src/python/include/Simplex_tree_interface.h @@ -164,11 +164,12 @@ class Simplex_tree_interface : public Simplex_tree { using Filtered_edge = std::tuple; std::vector edges; for (Simplex_handle sh : Base::skeleton_simplex_range(1)) { - typename Base::Simplex_vertex_range rg = Base::simplex_vertex_range(sh); - auto rg_begin = rg.begin(); - // We take only edges into account - if (std::distance(rg_begin, rg.end()) == 2) { - edges.emplace_back(*rg_begin, *std::next(rg_begin), Base::filtration(sh)); + if (Base::dimension(sh) == 1) { + typename Base::Simplex_vertex_range rg = Base::simplex_vertex_range(sh); + auto vit = rg.begin(); + Vertex_handle v = *vit; + Vertex_handle w = *++vit; + edges.emplace_back(v, w, Base::filtration(sh)); } } -- cgit v1.2.3 From 010e8625ace3bbbfd09b7946237e8f3f04159452 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Mon, 3 Aug 2020 09:14:35 +0200 Subject: code review: no need to test if pointer is NULL --- src/python/gudhi/simplex_tree.pyx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 62f81d0b..dfb1d985 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -600,5 +600,4 @@ cdef class SimplexTree: # New pointer is a new collapsed simplex tree self.thisptr = (ptr.collapse_edges(nb_iter)) # Delete old pointer - if ptr != NULL: - del ptr + del ptr -- cgit v1.2.3 From 9b52376e57c784838d07e15343e2a9194aad84cc Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Mon, 3 Aug 2020 10:48:08 +0200 Subject: doc review: specify the error and indicate what to install on Ubuntu --- src/python/doc/persistence_graphical_tools_user.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/doc/persistence_graphical_tools_user.rst b/src/python/doc/persistence_graphical_tools_user.rst index 5cdc2dca..9954058d 100644 --- a/src/python/doc/persistence_graphical_tools_user.rst +++ b/src/python/doc/persistence_graphical_tools_user.rst @@ -98,7 +98,21 @@ By default, persistence graphical tools are using LaTeX support for matplotlib i `matplotlib text rendering with LaTeX `_). It also requires `type1cm` LaTeX package (not detected by matplotlib). -If you are facing issues with LaTeX rendering, you can still deactivate LaTeX rendering by saying: +If you are facing issues with LaTeX rendering, like this one: + +.. code-block:: none + + Traceback (most recent call last): + File "/usr/lib/python3/dist-packages/matplotlib/texmanager.py", line 302, in _run_checked_subprocess + report = subprocess.check_output(command, + ... + ! LaTeX Error: File `type1cm.sty' not found. + ... + +This is because the LaTeX package is not installed on your system. On Ubuntu systems you can install texlive-full +(for all LaTeX packages), or more specific packages like texlive-latex-extra, cm-super. + +You can still deactivate LaTeX rendering by saying: .. code-block:: python -- cgit v1.2.3 From 39aff7e55beb318d1685dac410466414e69190ae Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Mon, 3 Aug 2020 16:22:59 +0200 Subject: doc review: Add LaTeX in optional runtime dependencies --- src/python/doc/installation.rst | 29 ++++++++++++++++++++++ .../doc/persistence_graphical_tools_user.rst | 21 ++-------------- 2 files changed, 31 insertions(+), 19 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/installation.rst b/src/python/doc/installation.rst index 525ca84e..78e1af73 100644 --- a/src/python/doc/installation.rst +++ b/src/python/doc/installation.rst @@ -323,6 +323,35 @@ The following examples require the `Matplotlib `_: * :download:`euclidean_strong_witness_complex_diagram_persistence_from_off_file_example.py <../example/euclidean_strong_witness_complex_diagram_persistence_from_off_file_example.py>` * :download:`euclidean_witness_complex_diagram_persistence_from_off_file_example.py <../example/euclidean_witness_complex_diagram_persistence_from_off_file_example.py>` +LaTeX +~~~~~ + +If a sufficiently complete LaTeX toolchain is available (including dvipng and ghostscript), the LaTeX option of +matplotlib is enabled for prettier captions (cf. +`matplotlib text rendering with LaTeX `_). +It also requires `type1cm` LaTeX package (not detected by matplotlib). + +If you are facing issues with LaTeX rendering, like this one: + +.. code-block:: none + + Traceback (most recent call last): + File "/usr/lib/python3/dist-packages/matplotlib/texmanager.py", line 302, in _run_checked_subprocess + report = subprocess.check_output(command, + ... + ! LaTeX Error: File `type1cm.sty' not found. + ... + +This is because the LaTeX package is not installed on your system. On Ubuntu systems you can install texlive-full +(for all LaTeX packages), or more specific packages like texlive-latex-extra, cm-super. + +You can still deactivate LaTeX rendering by saying: + +.. code-block:: python + + import gudhi + gudhi.persistence_graphical_tools._gudhi_matplotlib_use_tex=False + PyKeOps ------- diff --git a/src/python/doc/persistence_graphical_tools_user.rst b/src/python/doc/persistence_graphical_tools_user.rst index 9954058d..d95b9d2b 100644 --- a/src/python/doc/persistence_graphical_tools_user.rst +++ b/src/python/doc/persistence_graphical_tools_user.rst @@ -94,25 +94,8 @@ If you want more information on a specific dimension, for instance: LaTeX support ------------- -By default, persistence graphical tools are using LaTeX support for matplotlib if available (cf. -`matplotlib text rendering with LaTeX `_). -It also requires `type1cm` LaTeX package (not detected by matplotlib). - -If you are facing issues with LaTeX rendering, like this one: - -.. code-block:: none - - Traceback (most recent call last): - File "/usr/lib/python3/dist-packages/matplotlib/texmanager.py", line 302, in _run_checked_subprocess - report = subprocess.check_output(command, - ... - ! LaTeX Error: File `type1cm.sty' not found. - ... - -This is because the LaTeX package is not installed on your system. On Ubuntu systems you can install texlive-full -(for all LaTeX packages), or more specific packages like texlive-latex-extra, cm-super. - -You can still deactivate LaTeX rendering by saying: +If you are facing issues with `LaTeX `_ rendering, you can still deactivate LaTeX rendering by +saying: .. code-block:: python -- cgit v1.2.3 From e36fa6c9511c387447ef77e062e26671505212a2 Mon Sep 17 00:00:00 2001 From: MathieuCarriere Date: Mon, 3 Aug 2020 12:05:38 -0400 Subject: fix wasserstein autodiff --- src/python/gudhi/wasserstein/wasserstein.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index b37d30bb..fe001c37 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -165,9 +165,9 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab # empty arrays are not handled properly by the helpers, so we avoid calling them if len(pairs_X_Y): dists.append((Y_orig[pairs_X_Y[:, 1]] - X_orig[pairs_X_Y[:, 0]]).norms.lp(internal_p, axis=-1).norms.lp(order)) - if len(pairs_X_diag): + if len(pairs_X_diag[0]): dists.append(_perstot_autodiff(X_orig[pairs_X_diag], order, internal_p)) - if len(pairs_Y_diag): + if len(pairs_Y_diag[0]): dists.append(_perstot_autodiff(Y_orig[pairs_Y_diag], order, internal_p)) dists = [dist.reshape(1) for dist in dists] return ep.concatenate(dists).norms.lp(order).raw -- cgit v1.2.3 From 47f7f50c8cdb40a0a1fd73432498004f21641803 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Tue, 4 Aug 2020 12:46:06 +0200 Subject: Remove JAX from the documentation jax.grad does not work with our functions (I think it used to work...) --- src/python/gudhi/point_cloud/knn.py | 2 +- src/python/gudhi/wasserstein/wasserstein.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/point_cloud/knn.py b/src/python/gudhi/point_cloud/knn.py index 4652fe80..994be3b6 100644 --- a/src/python/gudhi/point_cloud/knn.py +++ b/src/python/gudhi/point_cloud/knn.py @@ -46,7 +46,7 @@ class KNearestNeighbors: sort_results (bool): if True, then distances and indices of each point are sorted on return, so that the first column contains the closest points. Otherwise, neighbors are returned in an arbitrary order. Defaults to True. - enable_autodiff (bool): if the input is a torch.tensor, jax.numpy.ndarray or tensorflow.Tensor, this + enable_autodiff (bool): if the input is a torch.tensor or tensorflow.Tensor, this instructs the function to compute distances in a way that works with automatic differentiation. This is experimental, not supported for all metrics, and requires the package EagerPy. Defaults to False. diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index fe001c37..a9d1cdff 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -99,7 +99,7 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab :param order: exponent for Wasserstein; Default value is 1. :param internal_p: Ground metric on the (upper-half) plane (i.e. norm L^p in R^2); Default value is `np.inf`. - :param enable_autodiff: If X and Y are torch.tensor, tensorflow.Tensor or jax.numpy.ndarray, make the computation + :param enable_autodiff: If X and Y are torch.tensor or tensorflow.Tensor, make the computation transparent to automatic differentiation. This requires the package EagerPy and is currently incompatible with `matching=True`. -- cgit v1.2.3 From 4cbe978277d8d4fd81ef91bf26f65b5d9b279cf0 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 4 Aug 2020 16:55:52 +0200 Subject: Fix python alpha complex for conda package --- src/python/test/test_alpha_complex.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'src/python') diff --git a/src/python/test/test_alpha_complex.py b/src/python/test/test_alpha_complex.py index a4ee260b..814f8289 100755 --- a/src/python/test/test_alpha_complex.py +++ b/src/python/test/test_alpha_complex.py @@ -198,8 +198,7 @@ def test_delaunay_complex(): _delaunay_complex(precision) def _3d_points_on_a_plane(precision, default_filtration_value): - alpha = gd.AlphaComplex(off_file=gd.__root_source_dir__ + '/data/points/alphacomplexdoc.off', - precision = precision) + alpha = gd.AlphaComplex(off_file='alphacomplexdoc.off', precision = precision) simplex_tree = alpha.create_simplex_tree(default_filtration_value = default_filtration_value) assert simplex_tree.dimension() == 2 @@ -207,6 +206,18 @@ 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) -- cgit v1.2.3