From 74155081bb8b3330c562d5c40d7f0a32fc188012 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Mon, 30 Mar 2020 18:02:43 +0200 Subject: Add density estimator --- src/python/gudhi/point_cloud/dtm.py | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index 23c36b88..e12eefa1 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -8,6 +8,7 @@ # - YYYY/MM Author: Description of the modification from .knn import KNN +import numpy as np class DTM: @@ -54,3 +55,68 @@ class DTM: # We compute too many powers, 1/p in knn then q in dtm, 1/q in dtm then q or some log in the caller. # Add option to skip the final root? return dtm + + +class DTMDensity: + """ + Density estimator based on the distance to the empirical measure defined by a point set, as defined in :cite:`dtmdensity`. Note that this implementation does not renormalize so the total measure is not 1, see the reference for suitable normalization factors in the Euclidean case. + """ + + def __init__(self, k=None, weights=None, q=None, dim=None, **kwargs): + """ + Args: + k (int): number of neighbors (possibly including the point itself). + weights (numpy.array): weights of each of the k neighbors, optional. + q (float): order used to compute the distance to measure. Defaults to dim. + dim (float): final exponent representing the dimension. Defaults to the dimension, and must be specified when the dimension cannot be read from the input (metric="neighbors" or metric="precomputed"). + kwargs: same parameters as :class:`~gudhi.point_cloud.knn.KNN`, except that metric="neighbors" means that :func:`transform` expects an array with the distances to the k nearest neighbors. + """ + if weights is None: + assert k is not None, "Must specify k or weights" + self.k = k + self.weights = np.full(k, 1.0 / k) + else: + self.weights = weights + self.k = len(weights) + assert k is None or k == self.k, "k differs from the length of weights" + self.q = q + self.dim = dim + self.params = kwargs + + def fit_transform(self, X, y=None): + return self.fit(X).transform(X) + + def fit(self, X, y=None): + """ + Args: + X (numpy.array): coordinates for mass points. + """ + if self.params.setdefault("metric", "euclidean") != "neighbors": + self.knn = KNN(self.k, return_index=False, return_distance=True, sort_results=False, **self.params) + self.knn.fit(X) + return self + + def transform(self, X): + """ + Args: + X (numpy.array): coordinates for query points, or distance matrix if metric is "precomputed", or distances to the k nearest neighbors if metric is "neighbors" (if the array has more than k columns, the remaining ones are ignored). + """ + q = self.q + dim = self.dim + if dim is None: + assert self.params["metric"] not in { + "neighbors", + "precomputed", + }, "dim not specified and cannot guess the dimension" + dim = len(X[0]) + if q is None: + q = dim + if self.params["metric"] == "neighbors": + distances = X[:, : self.k] + else: + distances = self.knn.transform(X) + distances = distances ** q + dtm = (distances * weights).sum(-1) + return dtm ** (-dim / q) + # We compute too many powers, 1/p in knn then q in dtm, d/q in dtm then whatever in the caller. + # Add option to skip the final root? -- cgit v1.2.3 From 9b66423fefca29e9e18f08d524b1fa0ce4db85a1 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Mon, 11 May 2020 19:13:44 +0200 Subject: Reformat doc --- src/python/gudhi/point_cloud/dtm.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index c5405526..ef6eef05 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -73,7 +73,9 @@ class DistanceToMeasure: class DTMDensity: """ - Density estimator based on the distance to the empirical measure defined by a point set, as defined in :cite:`dtmdensity`. Note that this implementation does not renormalize so the total measure is not 1, see the reference for suitable normalization factors in the Euclidean case. + Density estimator based on the distance to the empirical measure defined by a point set, as defined + in :cite:`dtmdensity`. Note that this implementation does not renormalize so the total measure is not 1, + see the reference for suitable normalization factors in the Euclidean case. """ def __init__(self, k=None, weights=None, q=None, dim=None, **kwargs): @@ -82,8 +84,10 @@ class DTMDensity: k (int): number of neighbors (possibly including the point itself). weights (numpy.array): weights of each of the k neighbors, optional. q (float): order used to compute the distance to measure. Defaults to dim. - dim (float): final exponent representing the dimension. Defaults to the dimension, and must be specified when the dimension cannot be read from the input (metric="neighbors" or metric="precomputed"). - kwargs: same parameters as :class:`~gudhi.point_cloud.knn.KNN`, except that metric="neighbors" means that :func:`transform` expects an array with the distances to the k nearest neighbors. + dim (float): final exponent representing the dimension. Defaults to the dimension, and must be specified + when the dimension cannot be read from the input (metric="neighbors" or metric="precomputed"). + kwargs: same parameters as :class:`~gudhi.point_cloud.knn.KNN`, except that metric="neighbors" means that + :func:`transform` expects an array with the distances to the k nearest neighbors. """ if weights is None: assert k is not None, "Must specify k or weights" @@ -113,7 +117,9 @@ class DTMDensity: def transform(self, X): """ Args: - X (numpy.array): coordinates for query points, or distance matrix if metric is "precomputed", or distances to the k nearest neighbors if metric is "neighbors" (if the array has more than k columns, the remaining ones are ignored). + X (numpy.array): coordinates for query points, or distance matrix if metric is "precomputed", + or distances to the k nearest neighbors if metric is "neighbors" (if the array has more + than k columns, the remaining ones are ignored). """ q = self.q dim = self.dim -- cgit v1.2.3 From d86676e247bfa6f29b625a9a5752bf2a2fab438f Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Mon, 11 May 2020 21:31:34 +0200 Subject: Normalize the density if asked --- src/python/gudhi/point_cloud/dtm.py | 38 +++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index ef6eef05..2f30908d 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -74,19 +74,27 @@ class DistanceToMeasure: class DTMDensity: """ Density estimator based on the distance to the empirical measure defined by a point set, as defined - in :cite:`dtmdensity`. Note that this implementation does not renormalize so the total measure is not 1, - see the reference for suitable normalization factors in the Euclidean case. + in :cite:`dtmdensity`. Note that this implementation only renormalizes when asked, and the renormalization + only works for a Euclidean metric, so in other cases the total measure may not be 1. """ - def __init__(self, k=None, weights=None, q=None, dim=None, **kwargs): + def __init__(self, k=None, weights=None, q=None, dim=None, normalize=False, n_samples=None, **kwargs): """ Args: k (int): number of neighbors (possibly including the point itself). - weights (numpy.array): weights of each of the k neighbors, optional. + weights (numpy.array): weights of each of the k neighbors, optional. They are supposed to sum to 1. q (float): order used to compute the distance to measure. Defaults to dim. dim (float): final exponent representing the dimension. Defaults to the dimension, and must be specified when the dimension cannot be read from the input (metric="neighbors" or metric="precomputed"). - kwargs: same parameters as :class:`~gudhi.point_cloud.knn.KNN`, except that metric="neighbors" means that + + .. note:: when the dimension is high, using it as an exponent can quickly lead to under- or overflows. + We recommend using a small fixed value instead in those cases, even if it won't have the same nice + theoretical properties as the dimension. + normalize (bool): normalize the density so it corresponds to a probability measure on ℝᵈ. + Only available for the Euclidean metric, defaults to False. + n_samples (int): number of sample points used for fitting. Only needed if `normalize` is True and + metric is "neighbors". + kwargs: same parameters as :class:`~gudhi.point_cloud.knn.KNearestNeighbors`, except that metric="neighbors" means that :func:`transform` expects an array with the distances to the k nearest neighbors. """ if weights is None: @@ -100,6 +108,8 @@ class DTMDensity: self.q = q self.dim = dim self.params = kwargs + self.normalize=normalize + self.n_samples=n_samples def fit_transform(self, X, y=None): return self.fit(X).transform(X) @@ -110,8 +120,10 @@ class DTMDensity: X (numpy.array): coordinates for mass points. """ if self.params.setdefault("metric", "euclidean") != "neighbors": - self.knn = KNN(self.k, return_index=False, return_distance=True, sort_results=False, **self.params) + self.knn = KNearestNeighbors(self.k, return_index=False, return_distance=True, sort_results=False, **self.params) self.knn.fit(X) + if self.params["metric"] != "precomputed": + self.n_samples = len(X) return self def transform(self, X): @@ -136,7 +148,17 @@ class DTMDensity: else: distances = self.knn.transform(X) distances = distances ** q - dtm = (distances * weights).sum(-1) - return dtm ** (-dim / q) + dtm = (distances * self.weights).sum(-1) + if self.normalize: + dtm /= (np.arange(1, self.k + 1) ** (q / dim) * self.weights).sum() + density = dtm ** (-dim / q) + if self.normalize: + import math + if self.params["metric"] == "precomputed": + self.n_samples = len(X[0]) + # Volume of d-ball + Vd = math.pi ** (dim / 2) / math.gamma(dim / 2 + 1) + density /= self.n_samples * Vd + return density # We compute too many powers, 1/p in knn then q in dtm, d/q in dtm then whatever in the caller. # Add option to skip the final root? -- cgit v1.2.3 From 8c9a1c674dcacc8b66e88897b6116561bb811ffa Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Mon, 11 May 2020 21:55:21 +0200 Subject: Handle k=1 in KNearestNeighbors with SciPy --- src/python/gudhi/point_cloud/knn.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/python/gudhi/point_cloud/knn.py b/src/python/gudhi/point_cloud/knn.py index 34e80b5d..65896847 100644 --- a/src/python/gudhi/point_cloud/knn.py +++ b/src/python/gudhi/point_cloud/knn.py @@ -302,6 +302,10 @@ class KNearestNeighbors: if self.params["implementation"] == "ckdtree": qargs = {key: val for key, val in self.params.items() if key in {"p", "eps", "n_jobs"}} distances, neighbors = self.kdtree.query(X, k=self.k, **qargs) + if k == 1: + # SciPy decided to squeeze the last dimension for k=1 + distances = distances[:, None] + neighbors = neighbors[:, None] if self.return_index: if self.return_distance: return neighbors, distances -- cgit v1.2.3 From 7bbbe63ffa2a812dc49c37c77b4f4a4be46b2a49 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Mon, 11 May 2020 23:34:23 +0200 Subject: move note --- src/python/gudhi/point_cloud/dtm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index 2f30908d..f8cca2c1 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -76,6 +76,10 @@ class DTMDensity: Density estimator based on the distance to the empirical measure defined by a point set, as defined in :cite:`dtmdensity`. Note that this implementation only renormalizes when asked, and the renormalization only works for a Euclidean metric, so in other cases the total measure may not be 1. + + .. note:: When the dimension is high, using it as an exponent can quickly lead to under- or overflows. + We recommend using a small fixed value instead in those cases, even if it won't have the same nice + theoretical properties as the dimension. """ def __init__(self, k=None, weights=None, q=None, dim=None, normalize=False, n_samples=None, **kwargs): @@ -86,10 +90,6 @@ class DTMDensity: q (float): order used to compute the distance to measure. Defaults to dim. dim (float): final exponent representing the dimension. Defaults to the dimension, and must be specified when the dimension cannot be read from the input (metric="neighbors" or metric="precomputed"). - - .. note:: when the dimension is high, using it as an exponent can quickly lead to under- or overflows. - We recommend using a small fixed value instead in those cases, even if it won't have the same nice - theoretical properties as the dimension. normalize (bool): normalize the density so it corresponds to a probability measure on ℝᵈ. Only available for the Euclidean metric, defaults to False. n_samples (int): number of sample points used for fitting. Only needed if `normalize` is True and -- cgit v1.2.3 From c87a1f10e048477d210ae0abd657da87bba1102a Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Tue, 12 May 2020 20:36:38 +0200 Subject: test + reformat --- src/python/gudhi/point_cloud/dtm.py | 9 ++++++--- src/python/test/test_dtm.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index f8cca2c1..4454d8a2 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -108,8 +108,8 @@ class DTMDensity: self.q = q self.dim = dim self.params = kwargs - self.normalize=normalize - self.n_samples=n_samples + self.normalize = normalize + self.n_samples = n_samples def fit_transform(self, X, y=None): return self.fit(X).transform(X) @@ -120,7 +120,9 @@ class DTMDensity: X (numpy.array): coordinates for mass points. """ if self.params.setdefault("metric", "euclidean") != "neighbors": - self.knn = KNearestNeighbors(self.k, return_index=False, return_distance=True, sort_results=False, **self.params) + self.knn = KNearestNeighbors( + self.k, return_index=False, return_distance=True, sort_results=False, **self.params + ) self.knn.fit(X) if self.params["metric"] != "precomputed": self.n_samples = len(X) @@ -154,6 +156,7 @@ class DTMDensity: density = dtm ** (-dim / q) if self.normalize: import math + if self.params["metric"] == "precomputed": self.n_samples = len(X[0]) # Volume of d-ball diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index bff4c267..34d28d4d 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -8,10 +8,11 @@ - YYYY/MM Author: Description of the modification """ -from gudhi.point_cloud.dtm import DistanceToMeasure +from gudhi.point_cloud.dtm import DistanceToMeasure, DTMDensity import numpy import pytest import torch +import math def test_dtm_compare_euclidean(): @@ -66,3 +67,11 @@ def test_dtm_precomputed(): dtm = DistanceToMeasure(2, q=2, metric="neighbors") r = dtm.fit_transform(dist) assert r == pytest.approx([2.0, 0.707, 3.5355], rel=0.01) + + +def test_density_normalized(): + sample = numpy.random.normal(0, 1, (1000000, 2)) + queries = numpy.array([[0.0, 0.0], [-0.5, 0.7], [0.4, 1.7]]) + expected = numpy.exp(-(queries ** 2).sum(-1) / 2) / (2 * math.pi) + estimated = DTMDensity(k=150, normalize=True).fit(sample).transform(queries) + assert estimated == pytest.approx(expected, rel=0.4) -- cgit v1.2.3 From c5fca5477cc6fff77acedf7b5324eb5f8b417ed3 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Tue, 12 May 2020 22:31:42 +0200 Subject: More test --- src/python/doc/point_cloud_sum.inc | 4 ++-- src/python/gudhi/point_cloud/dtm.py | 4 ++-- src/python/test/test_dtm.py | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/python/doc/point_cloud_sum.inc b/src/python/doc/point_cloud_sum.inc index d4761aba..d28f387a 100644 --- a/src/python/doc/point_cloud_sum.inc +++ b/src/python/doc/point_cloud_sum.inc @@ -3,8 +3,8 @@ +----------------------------------------------------------------+------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+ | | :math:`(x_1, x_2, \ldots, x_d)` | Utilities to process point clouds: read from file, subsample, | :Authors: Vincent Rouvreau, Marc Glisse, Masatoshi Takenouchi | - | | :math:`(y_1, y_2, \ldots, y_d)` | find neighbors, embed time series in higher dimension, etc. | | - | | | :Since: GUDHI 2.0.0 | + | | :math:`(y_1, y_2, \ldots, y_d)` | find neighbors, embed time series in higher dimension, estimate | | + | | a density, etc. | :Since: GUDHI 2.0.0 | | | | | | | | :License: MIT (`GPL v3 `_, BSD-3-Clause, Apache-2.0) | | | Parts of this package require CGAL. | | diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index 4454d8a2..88f197e7 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -89,7 +89,7 @@ class DTMDensity: weights (numpy.array): weights of each of the k neighbors, optional. They are supposed to sum to 1. q (float): order used to compute the distance to measure. Defaults to dim. dim (float): final exponent representing the dimension. Defaults to the dimension, and must be specified - when the dimension cannot be read from the input (metric="neighbors" or metric="precomputed"). + when the dimension cannot be read from the input (metric is "neighbors" or "precomputed"). normalize (bool): normalize the density so it corresponds to a probability measure on ℝᵈ. Only available for the Euclidean metric, defaults to False. n_samples (int): number of sample points used for fitting. Only needed if `normalize` is True and @@ -146,7 +146,7 @@ class DTMDensity: if q is None: q = dim if self.params["metric"] == "neighbors": - distances = X[:, : self.k] + distances = np.asarray(X)[:, : self.k] else: distances = self.knn.transform(X) distances = distances ** q diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index 34d28d4d..8ab0cc44 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -75,3 +75,10 @@ def test_density_normalized(): expected = numpy.exp(-(queries ** 2).sum(-1) / 2) / (2 * math.pi) estimated = DTMDensity(k=150, normalize=True).fit(sample).transform(queries) assert estimated == pytest.approx(expected, rel=0.4) + + +def test_density(): + distances = [[0, 1, 10], [2, 0, 30], [1, 3, 5]] + density = DTMDensity(k=2, metric="neighbors", dim=1).fit_transform(distances) + expected = numpy.array([2.0, 1.0, 0.5]) + assert density == pytest.approx(expected) -- cgit v1.2.3 From 84b823b6436746a06cb8323fecd7b1f38d7ba244 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Sat, 16 May 2020 13:30:19 +0200 Subject: Minimal nogil support for cubical complexes --- src/python/gudhi/cubical_complex.pyx | 29 +++++++++++++++------------ src/python/gudhi/periodic_cubical_complex.pyx | 29 +++++++++++++++------------ 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/python/gudhi/cubical_complex.pyx b/src/python/gudhi/cubical_complex.pyx index ca979eda..308b5099 100644 --- a/src/python/gudhi/cubical_complex.pyx +++ b/src/python/gudhi/cubical_complex.pyx @@ -27,20 +27,20 @@ __license__ = "MIT" cdef extern from "Cubical_complex_interface.h" namespace "Gudhi": cdef cppclass Bitmap_cubical_complex_base_interface "Gudhi::Cubical_complex::Cubical_complex_interface<>": - Bitmap_cubical_complex_base_interface(vector[unsigned] dimensions, vector[double] top_dimensional_cells) - Bitmap_cubical_complex_base_interface(string perseus_file) - int num_simplices() - int dimension() + Bitmap_cubical_complex_base_interface(vector[unsigned] dimensions, vector[double] top_dimensional_cells) nogil + Bitmap_cubical_complex_base_interface(string perseus_file) nogil + int num_simplices() nogil + int dimension() nogil cdef extern from "Persistent_cohomology_interface.h" namespace "Gudhi": cdef cppclass Cubical_complex_persistence_interface "Gudhi::Persistent_cohomology_interface>": - Cubical_complex_persistence_interface(Bitmap_cubical_complex_base_interface * st, bool persistence_dim_max) - void compute_persistence(int homology_coeff_field, double min_persistence) - vector[pair[int, pair[double, double]]] get_persistence() - vector[vector[int]] cofaces_of_cubical_persistence_pairs() - vector[int] betti_numbers() - vector[int] persistent_betti_numbers(double from_value, double to_value) - vector[pair[double,double]] intervals_in_dimension(int dimension) + Cubical_complex_persistence_interface(Bitmap_cubical_complex_base_interface * st, bool persistence_dim_max) nogil + void compute_persistence(int homology_coeff_field, double min_persistence) nogil + vector[pair[int, pair[double, double]]] get_persistence() nogil + vector[vector[int]] cofaces_of_cubical_persistence_pairs() nogil + vector[int] betti_numbers() nogil + vector[int] persistent_betti_numbers(double from_value, double to_value) nogil + vector[pair[double,double]] intervals_in_dimension(int dimension) nogil # CubicalComplex python interface cdef class CubicalComplex: @@ -151,8 +151,11 @@ cdef class CubicalComplex: if self.pcohptr != NULL: del self.pcohptr assert self.__is_defined() - self.pcohptr = new Cubical_complex_persistence_interface(self.thisptr, True) - self.pcohptr.compute_persistence(homology_coeff_field, min_persistence) + cdef int field = homology_coeff_field + cdef double minp = min_persistence + with nogil: + self.pcohptr = new Cubical_complex_persistence_interface(self.thisptr, 1) + self.pcohptr.compute_persistence(field, minp) def persistence(self, homology_coeff_field=11, min_persistence=0): """This function computes and returns the persistence of the complex. diff --git a/src/python/gudhi/periodic_cubical_complex.pyx b/src/python/gudhi/periodic_cubical_complex.pyx index 06309772..dcca7b63 100644 --- a/src/python/gudhi/periodic_cubical_complex.pyx +++ b/src/python/gudhi/periodic_cubical_complex.pyx @@ -24,20 +24,20 @@ __license__ = "MIT" cdef extern from "Cubical_complex_interface.h" namespace "Gudhi": cdef cppclass Periodic_cubical_complex_base_interface "Gudhi::Cubical_complex::Cubical_complex_interface>": - Periodic_cubical_complex_base_interface(vector[unsigned] dimensions, vector[double] top_dimensional_cells, vector[bool] periodic_dimensions) - Periodic_cubical_complex_base_interface(string perseus_file) - int num_simplices() - int dimension() + Periodic_cubical_complex_base_interface(vector[unsigned] dimensions, vector[double] top_dimensional_cells, vector[bool] periodic_dimensions) nogil + Periodic_cubical_complex_base_interface(string perseus_file) nogil + int num_simplices() nogil + int dimension() nogil cdef extern from "Persistent_cohomology_interface.h" namespace "Gudhi": cdef cppclass Periodic_cubical_complex_persistence_interface "Gudhi::Persistent_cohomology_interface>>": - Periodic_cubical_complex_persistence_interface(Periodic_cubical_complex_base_interface * st, bool persistence_dim_max) - void compute_persistence(int homology_coeff_field, double min_persistence) - vector[pair[int, pair[double, double]]] get_persistence() - vector[vector[int]] cofaces_of_cubical_persistence_pairs() - vector[int] betti_numbers() - vector[int] persistent_betti_numbers(double from_value, double to_value) - vector[pair[double,double]] intervals_in_dimension(int dimension) + Periodic_cubical_complex_persistence_interface(Periodic_cubical_complex_base_interface * st, bool persistence_dim_max) nogil + void compute_persistence(int homology_coeff_field, double min_persistence) nogil + vector[pair[int, pair[double, double]]] get_persistence() nogil + vector[vector[int]] cofaces_of_cubical_persistence_pairs() nogil + vector[int] betti_numbers() nogil + vector[int] persistent_betti_numbers(double from_value, double to_value) nogil + vector[pair[double,double]] intervals_in_dimension(int dimension) nogil # PeriodicCubicalComplex python interface cdef class PeriodicCubicalComplex: @@ -156,8 +156,11 @@ cdef class PeriodicCubicalComplex: if self.pcohptr != NULL: del self.pcohptr assert self.__is_defined() - self.pcohptr = new Periodic_cubical_complex_persistence_interface(self.thisptr, True) - self.pcohptr.compute_persistence(homology_coeff_field, min_persistence) + cdef int field = homology_coeff_field + cdef double minp = min_persistence + with nogil: + self.pcohptr = new Periodic_cubical_complex_persistence_interface(self.thisptr, 1) + self.pcohptr.compute_persistence(field, minp) def persistence(self, homology_coeff_field=11, min_persistence=0): """This function computes and returns the persistence of the complex. -- cgit v1.2.3 From 207050fb1f5af375a98c70dbd5fc22149d6f6e22 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Sat, 16 May 2020 14:08:23 +0200 Subject: nogil for cubical constructor There may be some extra copying until cython3, but it is probably not that bad. --- src/python/gudhi/cubical_complex.pyx | 14 +++++++++++--- src/python/gudhi/periodic_cubical_complex.pyx | 18 +++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/python/gudhi/cubical_complex.pyx b/src/python/gudhi/cubical_complex.pyx index 308b5099..0068f2ff 100644 --- a/src/python/gudhi/cubical_complex.pyx +++ b/src/python/gudhi/cubical_complex.pyx @@ -80,7 +80,7 @@ cdef class CubicalComplex: perseus_file=''): if ((dimensions is not None) and (top_dimensional_cells is not None) and (perseus_file == '')): - self.thisptr = new Bitmap_cubical_complex_base_interface(dimensions, top_dimensional_cells) + self._construct_from_cells(dimensions, top_dimensional_cells) elif ((dimensions is None) and (top_dimensional_cells is not None) and (perseus_file == '')): top_dimensional_cells = np.array(top_dimensional_cells, @@ -88,11 +88,11 @@ cdef class CubicalComplex: order = 'F') dimensions = top_dimensional_cells.shape top_dimensional_cells = top_dimensional_cells.ravel(order='F') - self.thisptr = new Bitmap_cubical_complex_base_interface(dimensions, top_dimensional_cells) + self._construct_from_cells(dimensions, top_dimensional_cells) elif ((dimensions is None) and (top_dimensional_cells is None) and (perseus_file != '')): if os.path.isfile(perseus_file): - self.thisptr = new Bitmap_cubical_complex_base_interface(perseus_file.encode('utf-8')) + self._construct_from_file(perseus_file.encode('utf-8')) else: raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), perseus_file) @@ -101,6 +101,14 @@ cdef class CubicalComplex: "top_dimensional_cells or from a Perseus-style file name.", file=sys.stderr) + def _construct_from_cells(self, vector[unsigned] dimensions, vector[double] top_dimensional_cells): + with nogil: + self.thisptr = new Bitmap_cubical_complex_base_interface(dimensions, top_dimensional_cells) + + def _construct_from_file(self, string filename): + with nogil: + self.thisptr = new Bitmap_cubical_complex_base_interface(filename) + def __dealloc__(self): if self.thisptr != NULL: del self.thisptr diff --git a/src/python/gudhi/periodic_cubical_complex.pyx b/src/python/gudhi/periodic_cubical_complex.pyx index dcca7b63..11e1766c 100644 --- a/src/python/gudhi/periodic_cubical_complex.pyx +++ b/src/python/gudhi/periodic_cubical_complex.pyx @@ -81,9 +81,7 @@ cdef class PeriodicCubicalComplex: periodic_dimensions=None, perseus_file=''): if ((dimensions is not None) and (top_dimensional_cells is not None) and (periodic_dimensions is not None) and (perseus_file == '')): - self.thisptr = new Periodic_cubical_complex_base_interface(dimensions, - top_dimensional_cells, - periodic_dimensions) + self._construct_from_cells(dimensions, top_dimensional_cells, periodic_dimensions) elif ((dimensions is None) and (top_dimensional_cells is not None) and (periodic_dimensions is not None) and (perseus_file == '')): top_dimensional_cells = np.array(top_dimensional_cells, @@ -91,13 +89,11 @@ cdef class PeriodicCubicalComplex: order = 'F') dimensions = top_dimensional_cells.shape top_dimensional_cells = top_dimensional_cells.ravel(order='F') - self.thisptr = new Periodic_cubical_complex_base_interface(dimensions, - top_dimensional_cells, - periodic_dimensions) + self._construct_from_cells(dimensions, top_dimensional_cells, periodic_dimensions) elif ((dimensions is None) and (top_dimensional_cells is None) and (periodic_dimensions is None) and (perseus_file != '')): if os.path.isfile(perseus_file): - self.thisptr = new Periodic_cubical_complex_base_interface(perseus_file.encode('utf-8')) + self._construct_from_file(perseus_file.encode('utf-8')) else: print("file " + perseus_file + " not found.", file=sys.stderr) else: @@ -106,6 +102,14 @@ cdef class PeriodicCubicalComplex: "top_dimensional_cells and periodic_dimensions or from " "a Perseus-style file name.", file=sys.stderr) + def _construct_from_cells(self, vector[unsigned] dimensions, vector[double] top_dimensional_cells, vector[bool] periodic_dimensions): + with nogil: + self.thisptr = new Periodic_cubical_complex_base_interface(dimensions, top_dimensional_cells, periodic_dimensions) + + def _construct_from_file(self, string filename): + with nogil: + self.thisptr = new Periodic_cubical_complex_base_interface(filename) + def __dealloc__(self): if self.thisptr != NULL: del self.thisptr -- cgit v1.2.3 From c156309dfd00c6180f2fd2dc03be159fd21c2626 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Sun, 17 May 2020 23:32:21 +0200 Subject: One more nogil in cubical --- src/python/gudhi/cubical_complex.pyx | 3 ++- src/python/gudhi/periodic_cubical_complex.pyx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/python/gudhi/cubical_complex.pyx b/src/python/gudhi/cubical_complex.pyx index 0068f2ff..3ace2517 100644 --- a/src/python/gudhi/cubical_complex.pyx +++ b/src/python/gudhi/cubical_complex.pyx @@ -215,7 +215,8 @@ cdef class CubicalComplex: cdef vector[vector[int]] persistence_result output = [[],[]] - persistence_result = self.pcohptr.cofaces_of_cubical_persistence_pairs() + with nogil: + persistence_result = self.pcohptr.cofaces_of_cubical_persistence_pairs() pr = np.array(persistence_result) ess_ind = np.argwhere(pr[:,2] == -1)[:,0] diff --git a/src/python/gudhi/periodic_cubical_complex.pyx b/src/python/gudhi/periodic_cubical_complex.pyx index 11e1766c..bed55101 100644 --- a/src/python/gudhi/periodic_cubical_complex.pyx +++ b/src/python/gudhi/periodic_cubical_complex.pyx @@ -214,7 +214,8 @@ cdef class PeriodicCubicalComplex: cdef vector[vector[int]] persistence_result if self.pcohptr != NULL: output = [[],[]] - persistence_result = self.pcohptr.cofaces_of_cubical_persistence_pairs() + with nogil: + persistence_result = self.pcohptr.cofaces_of_cubical_persistence_pairs() pr = np.array(persistence_result) ess_ind = np.argwhere(pr[:,2] == -1)[:,0] -- cgit v1.2.3 From 2287b727126ffb9fc47869ac9ed6b6bd61c6605a Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Mon, 18 May 2020 23:54:02 +0200 Subject: Infer k when we pass the distances to the nearest neighbors --- src/python/gudhi/point_cloud/dtm.py | 23 +++++++++++++++++------ src/python/test/test_dtm.py | 4 ++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index 88f197e7..d836c28d 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -85,7 +85,8 @@ class DTMDensity: def __init__(self, k=None, weights=None, q=None, dim=None, normalize=False, n_samples=None, **kwargs): """ Args: - k (int): number of neighbors (possibly including the point itself). + k (int): number of neighbors (possibly including the point itself). Optional if it can be guessed + from weights or metric="neighbors". weights (numpy.array): weights of each of the k neighbors, optional. They are supposed to sum to 1. q (float): order used to compute the distance to measure. Defaults to dim. dim (float): final exponent representing the dimension. Defaults to the dimension, and must be specified @@ -98,9 +99,12 @@ class DTMDensity: :func:`transform` expects an array with the distances to the k nearest neighbors. """ if weights is None: - assert k is not None, "Must specify k or weights" self.k = k - self.weights = np.full(k, 1.0 / k) + if k is None: + assert kwargs.get("metric") == "neighbors", 'Must specify k or weights, unless metric is "neighbors"' + self.weights = None + else: + self.weights = np.full(k, 1.0 / k) else: self.weights = weights self.k = len(weights) @@ -145,14 +149,21 @@ class DTMDensity: dim = len(X[0]) if q is None: q = dim + k = self.k + weights = self.weights if self.params["metric"] == "neighbors": - distances = np.asarray(X)[:, : self.k] + distances = np.asarray(X) + if weights is None: + k = distances.shape[1] + weights = np.full(k, 1.0 / k) + else: + distances = distances[:, :k] else: distances = self.knn.transform(X) distances = distances ** q - dtm = (distances * self.weights).sum(-1) + dtm = (distances * weights).sum(-1) if self.normalize: - dtm /= (np.arange(1, self.k + 1) ** (q / dim) * self.weights).sum() + dtm /= (np.arange(1, k + 1) ** (q / dim) * weights).sum() density = dtm ** (-dim / q) if self.normalize: import math diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index 8ab0cc44..8d400c7e 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -82,3 +82,7 @@ def test_density(): density = DTMDensity(k=2, metric="neighbors", dim=1).fit_transform(distances) expected = numpy.array([2.0, 1.0, 0.5]) assert density == pytest.approx(expected) + distances = [[0, 1], [2, 0], [1, 3]] + density = DTMDensity(metric="neighbors", dim=1).fit_transform(distances) + expected = numpy.array([2.0, 1.0, 0.5]) + assert density == pytest.approx(expected) -- cgit v1.2.3 From 35d5ac4e6bb79ec41b35c0df611207b9cd578f49 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Tue, 19 May 2020 18:10:16 +0200 Subject: Test with explicit weights and remove duplicated assignment --- src/python/test/test_dtm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index 8d400c7e..0a52279e 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -84,5 +84,6 @@ def test_density(): assert density == pytest.approx(expected) distances = [[0, 1], [2, 0], [1, 3]] density = DTMDensity(metric="neighbors", dim=1).fit_transform(distances) - expected = numpy.array([2.0, 1.0, 0.5]) + assert density == pytest.approx(expected) + density = DTMDensity(weights=[0.5, 0.5], metric="neighbors", dim=1).fit_transform(distances) assert density == pytest.approx(expected) -- cgit v1.2.3 From bb9b6b2a58d3b31a0e25d473339f2bde6430a52d Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Tue, 19 May 2020 20:16:32 +0200 Subject: long line --- src/python/gudhi/point_cloud/dtm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index d836c28d..55ac58e6 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -95,8 +95,9 @@ class DTMDensity: Only available for the Euclidean metric, defaults to False. n_samples (int): number of sample points used for fitting. Only needed if `normalize` is True and metric is "neighbors". - kwargs: same parameters as :class:`~gudhi.point_cloud.knn.KNearestNeighbors`, except that metric="neighbors" means that - :func:`transform` expects an array with the distances to the k nearest neighbors. + kwargs: same parameters as :class:`~gudhi.point_cloud.knn.KNearestNeighbors`, except that + metric="neighbors" means that :func:`transform` expects an array with the distances to + the k nearest neighbors. """ if weights is None: self.k = k -- cgit v1.2.3