From e1fb188885c155742802bfbfdb9f808e3945e8cd Mon Sep 17 00:00:00 2001 From: Michael Catanzaro Date: Thu, 19 Oct 2023 09:41:29 -0400 Subject: [PATCH] Update python versions in CI (#73) * Update python versions in CI * Run black * Fix transformer API to match sklearn * change collections.Iterable to typing.Iterable * Update tests --------- Co-authored-by: Michael Catanzaro --- .github/workflows/python-app.yml | 4 +- RELEASE.txt | 4 + persim/_version.py | 2 +- persim/images.py | 630 +++++++++++++++++++------------ persim/landscapes/transformer.py | 29 +- test/test_distances.py | 242 +++++------- test/test_landscapes.py | 105 ++++-- test/test_persim.py | 24 +- test/test_persistence_imager.py | 316 ++++++++++------ 9 files changed, 788 insertions(+), 568 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 7e5f863..8b55157 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 @@ -39,4 +39,4 @@ jobs: pytest --cov persim - name: Upload coverage results run: | - bash <(curl -s https://codecov.io/bash) \ No newline at end of file + bash <(curl -s https://codecov.io/bash) diff --git a/RELEASE.txt b/RELEASE.txt index 9a1cf36..c9db8b4 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -1,3 +1,7 @@ +0.3.2 + - Update codebase to support python 3.7 - 3.12. + - Change `PersistenceLandscaper` API for sklearn compatibility. + 0.3.1 - Fixed bug with repeated intervals in bottleneck - Tidied up API for indicating matchings for bottleneck and wasserstein, and updated notebook diff --git a/persim/_version.py b/persim/_version.py index 260c070..f9aa3e1 100644 --- a/persim/_version.py +++ b/persim/_version.py @@ -1 +1 @@ -__version__ = "0.3.1" +__version__ = "0.3.2" diff --git a/persim/images.py b/persim/images.py index 4c57a7e..8083367 100644 --- a/persim/images.py +++ b/persim/images.py @@ -1,51 +1,53 @@ from __future__ import division -from itertools import product -import collections import copy +from itertools import product +from typing import Iterable + +import matplotlib.pyplot as plt import numpy as np -from scipy.stats import norm import scipy.spatial as spatial -import matplotlib.pyplot as plt -from persim import images_kernels -from persim import images_weights -from joblib import Parallel, delayed from deprecated.sphinx import deprecated -from sklearn.base import TransformerMixin +from joblib import Parallel, delayed from matplotlib.collections import LineCollection from scipy.stats import multivariate_normal as mvn +from scipy.stats import norm +from sklearn.base import TransformerMixin + +from persim import images_kernels, images_weights __all__ = ["PersImage", "PersistenceImager"] + @deprecated( reason="""Replaced with the class :class:`persim.PersistenceImager`.""", version="0.1.5", ) class PersImage(TransformerMixin): - """ Initialize a persistence image generator. - -Parameters ----------- - - pixels : pair of ints like (int, int) - Tuple representing number of pixels in return image along x and y axis. - spread : float - Standard deviation of gaussian kernel. - specs : dict - Parameters for shape of image with respect to diagram domain. This is used if you would like images to have a particular range. Shaped like:: - - { - "maxBD": float, - "minBD": float - } + """Initialize a persistence image generator. - kernel_type : string or ... - TODO: Implement this feature. - Determine which type of kernel used in the convolution, or pass in custom kernel. Currently only implements Gaussian. - weighting_type : string or ... - TODO: Implement this feature. - Determine which type of weighting function used, or pass in custom weighting function. - Currently only implements linear weighting. + Parameters + ---------- + + pixels : pair of ints like (int, int) + Tuple representing number of pixels in return image along x and y axis. + spread : float + Standard deviation of gaussian kernel. + specs : dict + Parameters for shape of image with respect to diagram domain. This is used if you would like images to have a particular range. Shaped like:: + + { + "maxBD": float, + "minBD": float + } + + kernel_type : string or ... + TODO: Implement this feature. + Determine which type of kernel used in the convolution, or pass in custom kernel. Currently only implements Gaussian. + weighting_type : string or ... + TODO: Implement this feature. + Determine which type of weighting function used, or pass in custom weighting function. + Currently only implements linear weighting. """ def __init__( @@ -57,7 +59,6 @@ def __init__( weighting_type="linear", verbose=True, ): - self.specs = specs self.kernel_type = kernel_type self.weighting_type = weighting_type @@ -72,7 +73,7 @@ def __init__( ) def transform(self, diagrams): - """ Convert diagram or list of diagrams to a persistence image. + """Convert diagram or list of diagrams to a persistence image. Parameters ----------- @@ -86,7 +87,7 @@ def transform(self, diagrams): return np.zeros((self.nx, self.ny)) # if first entry of first entry is not iterable, then diagrams is singular and we need to make it a list of diagrams try: - singular = not isinstance(diagrams[0][0], collections.Iterable) + singular = not isinstance(diagrams[0][0], Iterable) except IndexError: singular = False @@ -98,10 +99,20 @@ def transform(self, diagrams): if not self.specs: self.specs = { - "maxBD": np.max([np.max(np.vstack((landscape, np.zeros((1, 2))))) - for landscape in landscapes] + [0]), - "minBD": np.min([np.min(np.vstack((landscape, np.zeros((1, 2))))) - for landscape in landscapes] + [0]), + "maxBD": np.max( + [ + np.max(np.vstack((landscape, np.zeros((1, 2))))) + for landscape in landscapes + ] + + [0] + ), + "minBD": np.min( + [ + np.min(np.vstack((landscape, np.zeros((1, 2))))) + for landscape in landscapes + ] + + [0] + ), } imgs = [self._transform(dgm) for dgm in landscapes] @@ -143,8 +154,8 @@ def _transform(self, landscape): return img def weighting(self, landscape=None): - """ Define a weighting function, - for stability results to hold, the function must be 0 at y=0. + """Define a weighting function, + for stability results to hold, the function must be 0 at y=0. """ # TODO: Implement a logistic function @@ -153,7 +164,7 @@ def weighting(self, landscape=None): if landscape is not None: if len(landscape) > 0: maxy = np.max(landscape[:, 1]) - else: + else: maxy = 1 def linear(interval): @@ -162,9 +173,9 @@ def linear(interval): return (1 / maxy) * d if landscape is not None else d def pw_linear(interval): - """ This is the function defined as w_b(t) in the original PI paper + """This is the function defined as w_b(t) in the original PI paper - Take b to be maxy/self.ny to effectively zero out the bottom pixel row + Take b to be maxy/self.ny to effectively zero out the bottom pixel row """ t = interval[1] @@ -180,8 +191,8 @@ def pw_linear(interval): return linear def kernel(self, spread=1): - """ This will return whatever kind of kernel we want to use. - Must have signature (ndarray size NxM, ndarray size 1xM) -> ndarray size Nx1 + """This will return whatever kind of kernel we want to use. + Must have signature (ndarray size NxM, ndarray size 1xM) -> ndarray size Nx1 """ # TODO: use self.kernel_type to choose function @@ -192,17 +203,15 @@ def gaussian(data, pixel): @staticmethod def to_landscape(diagram): - """ Convert a diagram to a landscape - (b,d) -> (b, d-b) + """Convert a diagram to a landscape + (b,d) -> (b, d-b) """ diagram[:, 1] -= diagram[:, 0] return diagram def show(self, imgs, ax=None): - """ Visualize the persistence image - - """ + """Visualize the persistence image""" ax = ax or plt.gca() @@ -244,25 +253,25 @@ class PersistenceImager(TransformerMixin): Printing a PersistenceImager() object will print its hyperparameters:: - + > print(pimgr) - + PersistenceImager(birth_range=(0.0, 1.0), pers_range=(0.0, 1.0), pixel_size=0.2, weight=persistence, weight_params={'n': 1.0}, kernel=gaussian, kernel_params={'sigma': [[1.0, 0.0], [0.0, 1.0]]}) PersistenceImager() attributes can be adjusted at or after instantiation. Updating attributes of a PersistenceImager() object will automatically update all other dependent attributes:: - + > pimgr.pixel_size = 0.1 > pimgr.birth_range = (0, 2) > print(pimgr) > print(pimgr.resolution) - + PersistenceImager(birth_range=(0.0, 2.0), pers_range=(0.0, 1.0), pixel_size=0.1, weight=persistence, weight_params={'n': 1.0}, kernel=gaussian, kernel_params={'sigma': [[1.0, 0.0], [0.0, 1.0]]}) (20, 10) The `fit()` method can be called on one or more (-,2) numpy.ndarrays to automatically determine the miniumum birth and persistence ranges needed to capture all persistence pairs. The ranges and resolution are automatically adjusted to accomodate the specified pixel size. The option `skew=True` specifies that the diagram is currently in birth-death coordinates and must first be transformed to birth-persistence coordinates:: - + > import numpy as np > pimgr = PersistenceImager(pixel_size=0.5) > pdgms = [np.array([[0.5, 0.8], [0.7, 2.2], [2.5, 4.0]]), @@ -271,7 +280,7 @@ class PersistenceImager(TransformerMixin): > pimgr.fit(pdgms, skew=True) > print(pimgr) > print(pimgr.resolution) - + PersistenceImager(birth_range=(0.1, 3.1), pers_range=(-8.326672684688674e-17, 2.5), pixel_size=0.5, weight=persistence, weight_params={'n': 1.0}, kernel=gaussian, kernel_params={'sigma': [[1.0, 0.0], [0.0, 1.0]]}) (6, 5) @@ -280,23 +289,31 @@ class PersistenceImager(TransformerMixin): > pimgs = pimgr.transform(pdgms, skew=True) > pimgs[0] - + array([[0.03999068, 0.05688393, 0.06672051, 0.06341749, 0.04820814], [0.04506697, 0.06556791, 0.07809764, 0.07495246, 0.05730671], [0.04454486, 0.06674611, 0.08104366, 0.07869919, 0.06058808], [0.04113063, 0.0636504 , 0.07884635, 0.07747833, 0.06005714], [0.03625436, 0.05757744, 0.07242608, 0.07180125, 0.05593626], - [0.02922239, 0.04712024, 0.05979033, 0.05956698, 0.04653357]]) + [0.02922239, 0.04712024, 0.05979033, 0.05956698, 0.04653357]]) Notes ----- [1] Adams et. al., "Persistence Images: A Stable Vector Representation of Persistent Homology," Journal of Machine Learning Research, vol. 18, pp. 1-35, 2017. http://www.jmlr.org/papers/volume18/16-337/16-337.pdf """ - - def __init__(self, birth_range=None, pers_range=None, pixel_size=None, weight=None, weight_params=None, kernel=None, kernel_params=None): - """ PersistenceImager constructor method - """ + + def __init__( + self, + birth_range=None, + pers_range=None, + pixel_size=None, + weight=None, + weight_params=None, + kernel=None, + kernel_params=None, + ): + """PersistenceImager constructor method""" # set defaults if birth_range is None: birth_range = (0.0, 1.0) @@ -309,13 +326,21 @@ def __init__(self, birth_range=None, pers_range=None, pixel_size=None, weight=No if kernel is None: kernel = images_kernels.gaussian if weight_params is None: - weight_params = {'n': 1.0} + weight_params = {"n": 1.0} if kernel_params is None: - kernel_params = {'sigma': [[1.0, 0.0], [0.0, 1.0]]} - + kernel_params = {"sigma": [[1.0, 0.0], [0.0, 1.0]]} + # validate parameters - self._validate_parameters(birth_range=birth_range, pers_range=pers_range, pixel_size=pixel_size, weight=weight, weight_params=weight_params, kernel=kernel, kernel_params=kernel_params) - + self._validate_parameters( + birth_range=birth_range, + pers_range=pers_range, + pixel_size=pixel_size, + weight=weight, + weight_params=weight_params, + kernel=kernel, + kernel_params=kernel_params, + ) + self.weight, self.kernel = self._ensure_callable(weight=weight, kernel=kernel) self.weight_params = weight_params self.kernel_params = kernel_params @@ -324,14 +349,17 @@ def __init__(self, birth_range=None, pers_range=None, pixel_size=None, weight=No self._pers_range = pers_range self._width = birth_range[1] - birth_range[0] self._height = pers_range[1] - pers_range[0] - self._resolution = (int(self._width / self._pixel_size), int(self._height / self._pixel_size)) + self._resolution = ( + int(self._width / self._pixel_size), + int(self._height / self._pixel_size), + ) self._create_mesh() - + @property def width(self): """ Persistence image width. - + Returns ------- width : float @@ -343,7 +371,7 @@ def width(self): def height(self): """ Persistence image height. - + Returns ------- height : float @@ -355,7 +383,7 @@ def height(self): def resolution(self): """ Persistence image resolution. - + Returns ------- resolution : pair of ints (width, height) @@ -367,27 +395,36 @@ def resolution(self): def pixel_size(self): """ Persistence image square pixel dimensions. - + Returns ------- pixel_size : float - The width (and height) in birth/persistence units of each square pixel in the persistence image. + The width (and height) in birth/persistence units of each square pixel in the persistence image. """ return self._pixel_size @pixel_size.setter def pixel_size(self, val): self._pixel_size = val - self._width = int(np.ceil((self.birth_range[1] - self.birth_range[0]) / self.pixel_size)) * self.pixel_size - self._height = int(np.ceil((self.pers_range[1] - self.pers_range[0]) / self.pixel_size)) * self.pixel_size - self._resolution = (int(self.width / self.pixel_size), int(self.height / self.pixel_size)) + self._width = ( + int(np.ceil((self.birth_range[1] - self.birth_range[0]) / self.pixel_size)) + * self.pixel_size + ) + self._height = ( + int(np.ceil((self.pers_range[1] - self.pers_range[0]) / self.pixel_size)) + * self.pixel_size + ) + self._resolution = ( + int(self.width / self.pixel_size), + int(self.height / self.pixel_size), + ) self._create_mesh() @property def birth_range(self): """ Range of birth values covered by the persistence image. - + Returns ------- birth_range : pair of floats (min. birth, max. birth) @@ -398,15 +435,21 @@ def birth_range(self): @birth_range.setter def birth_range(self, val): self._birth_range = val - self._width = int(np.ceil((self.birth_range[1] - self.birth_range[0]) / self.pixel_size)) * self._pixel_size - self._resolution = (int(self.width / self.pixel_size), int(self.height / self.pixel_size)) + self._width = ( + int(np.ceil((self.birth_range[1] - self.birth_range[0]) / self.pixel_size)) + * self._pixel_size + ) + self._resolution = ( + int(self.width / self.pixel_size), + int(self.height / self.pixel_size), + ) self._create_mesh() @property def pers_range(self): """ Range of persistence values covered by the persistence image. - + Returns ------- pers_range : pair of floats (min. persistence, max. persistence) @@ -417,111 +460,173 @@ def pers_range(self): @pers_range.setter def pers_range(self, val): self._pers_range = val - self._height = int(np.ceil((self.pers_range[1] - self.pers_range[0]) / self.pixel_size)) * self._pixel_size - self._resolution = (int(self.width / self.pixel_size), int(self.height / self.pixel_size)) + self._height = ( + int(np.ceil((self.pers_range[1] - self.pers_range[0]) / self.pixel_size)) + * self._pixel_size + ) + self._resolution = ( + int(self.width / self.pixel_size), + int(self.height / self.pixel_size), + ) self._create_mesh() - + def __repr__(self): import pprint as pp - params = tuple([self.birth_range, self.pers_range, self.pixel_size, self.weight.__name__, pp.pformat(self.weight_params), self.kernel.__name__, pp.pformat(self.kernel_params)]) - repr_str = 'PersistenceImager(birth_range=%s, pers_range=%s, pixel_size=%s, weight=%s, weight_params=%s, kernel=%s, kernel_params=%s)' % params + + params = tuple( + [ + self.birth_range, + self.pers_range, + self.pixel_size, + self.weight.__name__, + pp.pformat(self.weight_params), + self.kernel.__name__, + pp.pformat(self.kernel_params), + ] + ) + repr_str = ( + "PersistenceImager(birth_range=%s, pers_range=%s, pixel_size=%s, weight=%s, weight_params=%s, kernel=%s, kernel_params=%s)" + % params + ) return repr_str - + def _ensure_callable(self, weight=None, kernel=None): - valid_weights_dict = {'persistence': images_weights.persistence, 'linear_ramp': images_weights.linear_ramp} - valid_kernels_dict = {'gaussian': images_kernels.gaussian, 'uniform': images_kernels.uniform} - + valid_weights_dict = { + "persistence": images_weights.persistence, + "linear_ramp": images_weights.linear_ramp, + } + valid_kernels_dict = { + "gaussian": images_kernels.gaussian, + "uniform": images_kernels.uniform, + } + if isinstance(weight, str): weight = valid_weights_dict[weight] - + if isinstance(kernel, str): kernel = valid_kernels_dict[kernel] - + return weight, kernel - - def _validate_parameters(self, birth_range=None, pers_range=None, pixel_size=None, weight=None, weight_params=None, kernel=None, kernel_params=None): - valid_weights = ['persistence', 'linear_ramp'] - valid_kernels = ['gaussian', 'uniform'] - + + def _validate_parameters( + self, + birth_range=None, + pers_range=None, + pixel_size=None, + weight=None, + weight_params=None, + kernel=None, + kernel_params=None, + ): + valid_weights = ["persistence", "linear_ramp"] + valid_kernels = ["gaussian", "uniform"] + # validate birth_range if isinstance(birth_range, tuple): if len(birth_range) != 2: raise ValueError("birth_range must be a pair: (min. birth, max. birth)") - elif (not isinstance(birth_range[0], (int, float))) or (not isinstance(birth_range[1], (int, float))): - raise ValueError("birth_range must be a pair of numbers: (min. birth, max. birth)") + elif (not isinstance(birth_range[0], (int, float))) or ( + not isinstance(birth_range[1], (int, float)) + ): + raise ValueError( + "birth_range must be a pair of numbers: (min. birth, max. birth)" + ) else: raise ValueError("birth_range must be a tuple") - + # validate pers_range if isinstance(pers_range, tuple): if len(pers_range) != 2: - raise ValueError("pers_range must be a pair: (min. persistence, max. persistence)") - elif (not isinstance(pers_range[0], (int, float))) or (not isinstance(pers_range[1], (int, float))): - raise ValueError("pers_range must be a pair of numbers: (min. persistence, max. persistence)") + raise ValueError( + "pers_range must be a pair: (min. persistence, max. persistence)" + ) + elif (not isinstance(pers_range[0], (int, float))) or ( + not isinstance(pers_range[1], (int, float)) + ): + raise ValueError( + "pers_range must be a pair of numbers: (min. persistence, max. persistence)" + ) else: raise ValueError("pers_range must be a tuple") - + # validate pixel_size if not isinstance(pixel_size, (int, float)): - raise ValueError("pixel_size must be an int or float") - + raise ValueError("pixel_size must be an int or float") + # validate weight if not callable(weight): if isinstance(weight, str): - if weight not in ['persistence', 'linear_ramp']: - raise ValueError("weight must be callable or a str in %s" %valid_weights) + if weight not in ["persistence", "linear_ramp"]: + raise ValueError( + "weight must be callable or a str in %s" % valid_weights + ) else: raise ValueError("weight must be callable or a str") - + # validate weight_params if not isinstance(weight_params, dict): raise ValueError("weight_params must be a dict") - + # validate kernel if not callable(kernel): if isinstance(kernel, str): if kernel not in valid_kernels: - raise ValueError("kernel must be callable or a str in %s" %valid_kernels) + raise ValueError( + "kernel must be callable or a str in %s" % valid_kernels + ) else: raise ValueError("kernel must be callable or a str") - + # validate kernel_params if not isinstance(kernel_params, dict): raise ValueError("kernel_params must be a dict") - + def _create_mesh(self): # padding around specified image ranges as a result of incommensurable ranges and pixel width db = self._width - (self._birth_range[1] - self._birth_range[0]) dp = self._height - (self._pers_range[1] - self._pers_range[0]) # adjust image ranges to accommodate incommensurable ranges and pixel width - self._birth_range = (self._birth_range[0] - db / 2, self._birth_range[1] + db / 2) + self._birth_range = ( + self._birth_range[0] - db / 2, + self._birth_range[1] + db / 2, + ) self._pers_range = (self._pers_range[0] - dp / 2, self._pers_range[1] + dp / 2) - + # construct linear spaces defining pixel locations - self._bpnts = np.linspace(self._birth_range[0], self._birth_range[1] + self._pixel_size, - self._resolution[0] + 1, endpoint=False, dtype=np.float64) - self._ppnts = np.linspace(self._pers_range[0], self._pers_range[1] + self._pixel_size, - self._resolution[1] + 1, endpoint=False, dtype=np.float64) + self._bpnts = np.linspace( + self._birth_range[0], + self._birth_range[1] + self._pixel_size, + self._resolution[0] + 1, + endpoint=False, + dtype=np.float64, + ) + self._ppnts = np.linspace( + self._pers_range[0], + self._pers_range[1] + self._pixel_size, + self._resolution[1] + 1, + endpoint=False, + dtype=np.float64, + ) def fit(self, pers_dgms, skew=True): - """ Choose persistence image range parameters which minimally enclose all persistence pairs across one or more persistence diagrams. - + """Choose persistence image range parameters which minimally enclose all persistence pairs across one or more persistence diagrams. + Parameters ---------- pers_dgms : one or an iterable of (-,2) numpy.ndarrays Collection of one or more persistence diagrams. - skew : boolean + skew : boolean Flag indicating if diagram(s) need to first be converted to birth-persistence coordinates (default: True). """ min_birth = np.Inf max_birth = -np.Inf min_pers = np.Inf max_pers = -np.Inf - - # convert to a list of diagrams if necessary + + # convert to a list of diagrams if necessary pers_dgms, singular = self._ensure_iterable(pers_dgms) - + # loop over diagrams to determine the maximum extent of the pairs contained in the birth-persistence plane for pers_dgm in pers_dgms: pers_dgm = np.copy(pers_dgm) @@ -547,14 +652,14 @@ def fit(self, pers_dgms, skew=True): self.pers_range = (min_pers, max_pers) def transform(self, pers_dgms, skew=True, n_jobs=None): - """ Transform a persistence diagram or an iterable containing a collection of persistence diagrams into + """Transform a persistence diagram or an iterable containing a collection of persistence diagrams into persistence images. - + Parameters ---------- pers_dgms : one or an iterable of (-,2) numpy.ndarrays Collection of one or more persistence diagrams. - skew : boolean + skew : boolean Flag indicating if diagram(s) need to first be converted to birth-persistence coordinates (default: True). n_jobs : int Number of cores to use to transform a collection of persistence diagrams into persistence images (default: None, uses a single core). @@ -568,34 +673,60 @@ def transform(self, pers_dgms, skew=True, n_jobs=None): parallelize = True else: parallelize = False - + # if diagram is empty, return empty image if len(pers_dgms) == 0: return np.zeros(self.resolution) - - # convert to a list of diagrams if necessary + + # convert to a list of diagrams if necessary pers_dgms, singular = self._ensure_iterable(pers_dgms) - + if parallelize: - pers_imgs = Parallel(n_jobs=n_jobs)(delayed(_transform)(pers_dgm, skew, self.resolution, self.weight, self.weight_params, self.kernel, self.kernel_params, self._bpnts, self._ppnts) for pers_dgm in pers_dgms) + pers_imgs = Parallel(n_jobs=n_jobs)( + delayed(_transform)( + pers_dgm, + skew, + self.resolution, + self.weight, + self.weight_params, + self.kernel, + self.kernel_params, + self._bpnts, + self._ppnts, + ) + for pers_dgm in pers_dgms + ) else: - pers_imgs = [_transform(pers_dgm, skew=skew, resolution=self.resolution, weight=self.weight, weight_params=self.weight_params, kernel=self.kernel, kernel_params=self.kernel_params, _bpnts=self._bpnts, _ppnts=self._ppnts) for pers_dgm in pers_dgms] - + pers_imgs = [ + _transform( + pers_dgm, + skew=skew, + resolution=self.resolution, + weight=self.weight, + weight_params=self.weight_params, + kernel=self.kernel, + kernel_params=self.kernel_params, + _bpnts=self._bpnts, + _ppnts=self._ppnts, + ) + for pers_dgm in pers_dgms + ] + if singular: pers_imgs = pers_imgs[0] - + return pers_imgs def fit_transform(self, pers_dgms, skew=True): - """ Choose persistence image range parameters which minimally enclose all persistence pairs across one or more persistence diagrams and transform the persistence diagrams into persistence images. - + """Choose persistence image range parameters which minimally enclose all persistence pairs across one or more persistence diagrams and transform the persistence diagrams into persistence images. + Parameters ---------- pers_dgms : one or an iterable of (-,2) numpy.ndarray Collection of one or more persistence diagrams. - skew : boolean + skew : boolean Flag indicating if diagram(s) need to first be converted to birth-persistence coordinates (default: True). - + Returns ------- @@ -611,27 +742,27 @@ def fit_transform(self, pers_dgms, skew=True): pers_imgs = self.transform(pers_dgms, skew=skew) return pers_imgs - + def _ensure_iterable(self, pers_dgms): # if first entry of first entry is not iterable, then diagrams is singular and we need to make it a list of diagrams try: - singular = not isinstance(pers_dgms[0][0], collections.Iterable) + singular = not isinstance(pers_dgms[0][0], Iterable) except IndexError: singular = False if singular: pers_dgms = [pers_dgms] - + return pers_dgms, singular def plot_diagram(self, pers_dgm, skew=True, ax=None, out_file=None): - """ Plot a persistence diagram. - + """Plot a persistence diagram. + Parameters ---------- pers_dgm : (-,2) numpy.ndarray A persistence diagram. - skew : boolean + skew : boolean Flag indicating if diagram needs to first be converted to birth-persistence coordinates (default: True). ax : matplotlib.Axes Instance of a matplotlib.Axes object in which to plot (default: None, generates a new figure) @@ -647,9 +778,9 @@ def plot_diagram(self, pers_dgm, skew=True, ax=None, out_file=None): if skew: pers_dgm[:, 1] = pers_dgm[:, 1] - pers_dgm[:, 0] - ylabel = 'persistence' + ylabel = "persistence" else: - ylabel = 'death' + ylabel = "death" # setup plot range plot_buff_frac = 0.05 @@ -670,16 +801,29 @@ def plot_diagram(self, pers_dgm, skew=True, ax=None, out_file=None): ax.set_ylim(pmin, pmax) # compute reasonable line width for pixel overlay (initially 1/50th of the width of a pixel) - linewidth = (1/50 * self.pixel_size) * 72 * plt.gcf().bbox_inches.width * ax.get_position().width / \ - np.min((bmax - bmin, pmax - pmin)) + linewidth = ( + (1 / 50 * self.pixel_size) + * 72 + * plt.gcf().bbox_inches.width + * ax.get_position().width + / np.min((bmax - bmin, pmax - pmin)) + ) # plot the persistence image grid if skew: - hlines = np.column_stack(np.broadcast_arrays(self._bpnts[0], self._ppnts, self._bpnts[-1], self._ppnts)) - vlines = np.column_stack(np.broadcast_arrays(self._bpnts, self._ppnts[0], self._bpnts, self._ppnts[-1])) + hlines = np.column_stack( + np.broadcast_arrays( + self._bpnts[0], self._ppnts, self._bpnts[-1], self._ppnts + ) + ) + vlines = np.column_stack( + np.broadcast_arrays( + self._bpnts, self._ppnts[0], self._bpnts, self._ppnts[-1] + ) + ) lines = np.concatenate([hlines, vlines]).reshape(-1, 2, 2) - line_collection = LineCollection(lines, color='black', linewidths=linewidth) - ax.add_collection(line_collection) + line_collection = LineCollection(lines, color="black", linewidths=linewidth) + ax.add_collection(line_collection) # plot persistence diagram ax.scatter(pers_dgm[:, 0], pers_dgm[:, 1]) @@ -691,20 +835,19 @@ def plot_diagram(self, pers_dgm, skew=True, ax=None, out_file=None): ax.plot([min_diag, max_diag], [min_diag, max_diag]) # fix and label axes - ax.set_aspect('equal') - ax.set_xlabel('birth') + ax.set_aspect("equal") + ax.set_xlabel("birth") ax.set_ylabel(ylabel) # optionally save figure if out_file: - plt.savefig(out_file, bbox_inches='tight') - - return ax + plt.savefig(out_file, bbox_inches="tight") + return ax def plot_image(self, pers_img, ax=None, out_file=None): - """ Plot a persistence image. - + """Plot a persistence image. + Parameters ---------- pers_img : (M,N) numpy.ndarray @@ -720,89 +863,112 @@ def plot_image(self, pers_img, ax=None, out_file=None): The matplotlib.Axes which contains the persistence image """ ax = ax or plt.gca() - ax.matshow(pers_img.T, **{'origin': 'lower'}) + ax.matshow(pers_img.T, **{"origin": "lower"}) # fix and label axes - ax.set_xlabel('birth') - ax.set_ylabel('persistence') + ax.set_xlabel("birth") + ax.set_ylabel("persistence") ax.get_xaxis().set_ticks([]) ax.get_yaxis().set_ticks([]) # optionally save figure if out_file: - plt.savefig(out_file, bbox_inches='tight') - + plt.savefig(out_file, bbox_inches="tight") + return ax -def _transform(pers_dgm, skew=True, resolution=None, weight=None, weight_params=None, kernel=None, kernel_params=None, _bpnts=None, _ppnts=None): - """ Transform a persistence diagram into a persistence image. - - Parameters - ---------- - pers_dgm : (-,2) numpy.ndarray - A persistence diagrams. - skew : boolean - Flag indicating if diagram(s) need to first be converted to birth-persistence coordinates (default: True). - resolution : pair of ints - The number of pixels along the birth and persistence axes in the persistence image. - weight : callable - Function which weights the birth-persistence plane. - weight_params : dict - Arguments needed to specify the weight function. - kernel : callable - Cumulative distribution function defining the kernel. - kernel_params : dict - Arguments needed to specify the kernel function. - _bpnts : (N,) numpy.ndarray - The birth coordinates of the persistence image pixel locations. - _ppnts : (M,) numpy.ndarray - The persistence coordinates of the persistence image pixel locations. - - Returns - ------- - numpy.ndarray - (M,N) numpy.ndarray encoding the persistence image corresponding to pers_dgm. - """ - pers_dgm = np.copy(pers_dgm) - pers_img = np.zeros(resolution) - n = pers_dgm.shape[0] - general_flag = True +def _transform( + pers_dgm, + skew=True, + resolution=None, + weight=None, + weight_params=None, + kernel=None, + kernel_params=None, + _bpnts=None, + _ppnts=None, +): + """Transform a persistence diagram into a persistence image. - # if necessary convert from birth-death coordinates to birth-persistence coordinates - if skew: - pers_dgm[:, 1] = pers_dgm[:, 1] - pers_dgm[:, 0] + Parameters + ---------- + pers_dgm : (-,2) numpy.ndarray + A persistence diagrams. + skew : boolean + Flag indicating if diagram(s) need to first be converted to birth-persistence coordinates (default: True). + resolution : pair of ints + The number of pixels along the birth and persistence axes in the persistence image. + weight : callable + Function which weights the birth-persistence plane. + weight_params : dict + Arguments needed to specify the weight function. + kernel : callable + Cumulative distribution function defining the kernel. + kernel_params : dict + Arguments needed to specify the kernel function. + _bpnts : (N,) numpy.ndarray + The birth coordinates of the persistence image pixel locations. + _ppnts : (M,) numpy.ndarray + The persistence coordinates of the persistence image pixel locations. - # compute weights at each persistence pair - wts = weight(pers_dgm[:, 0], pers_dgm[:, 1], **weight_params) - - # handle the special case of a standard, isotropic Gaussian kernel - if kernel == images_kernels.gaussian: - general_flag = False - sigma = kernel_params['sigma'] - - # sigma is specified by a single variance - if isinstance(sigma, (int, float)): - sigma = np.array([[sigma, 0.0], [0.0, sigma]], dtype=np.float64) - - if (sigma[0][0] == sigma[1][1] and sigma[0][1] == 0.0): - sigma = np.sqrt(sigma[0][0]) - for i in range(n): - ncdf_b = images_kernels.norm_cdf((_bpnts - pers_dgm[i, 0]) / sigma) - ncdf_p = images_kernels.norm_cdf((_ppnts - pers_dgm[i, 1]) / sigma) - curr_img = ncdf_p[None, :] * ncdf_b[:, None] - pers_img += wts[i]*(curr_img[1:, 1:] - curr_img[:-1, 1:] - curr_img[1:, :-1] + curr_img[:-1, :-1]) - else: - general_flag = True + Returns + ------- + numpy.ndarray + (M,N) numpy.ndarray encoding the persistence image corresponding to pers_dgm. + """ + pers_dgm = np.copy(pers_dgm) + pers_img = np.zeros(resolution) + n = pers_dgm.shape[0] + general_flag = True + + # if necessary convert from birth-death coordinates to birth-persistence coordinates + if skew: + pers_dgm[:, 1] = pers_dgm[:, 1] - pers_dgm[:, 0] - # handle the general case - if general_flag: - bb, pp = np.meshgrid(_bpnts, _ppnts, indexing='ij') - bb = bb.flatten(order='C') - pp = pp.flatten(order='C') + # compute weights at each persistence pair + wts = weight(pers_dgm[:, 0], pers_dgm[:, 1], **weight_params) + + # handle the special case of a standard, isotropic Gaussian kernel + if kernel == images_kernels.gaussian: + general_flag = False + sigma = kernel_params["sigma"] + + # sigma is specified by a single variance + if isinstance(sigma, (int, float)): + sigma = np.array([[sigma, 0.0], [0.0, sigma]], dtype=np.float64) + + if sigma[0][0] == sigma[1][1] and sigma[0][1] == 0.0: + sigma = np.sqrt(sigma[0][0]) for i in range(n): - curr_img = np.reshape(kernel(bb, pp, mu=pers_dgm[i, :], **kernel_params), - (resolution[0]+1, resolution[1]+1), order='C') - pers_img += wts[i]*(curr_img[1:, 1:] - curr_img[:-1, 1:] - curr_img[1:, :-1] + curr_img[:-1, :-1]) + ncdf_b = images_kernels.norm_cdf((_bpnts - pers_dgm[i, 0]) / sigma) + ncdf_p = images_kernels.norm_cdf((_ppnts - pers_dgm[i, 1]) / sigma) + curr_img = ncdf_p[None, :] * ncdf_b[:, None] + pers_img += wts[i] * ( + curr_img[1:, 1:] + - curr_img[:-1, 1:] + - curr_img[1:, :-1] + + curr_img[:-1, :-1] + ) + else: + general_flag = True + + # handle the general case + if general_flag: + bb, pp = np.meshgrid(_bpnts, _ppnts, indexing="ij") + bb = bb.flatten(order="C") + pp = pp.flatten(order="C") + for i in range(n): + curr_img = np.reshape( + kernel(bb, pp, mu=pers_dgm[i, :], **kernel_params), + (resolution[0] + 1, resolution[1] + 1), + order="C", + ) + pers_img += wts[i] * ( + curr_img[1:, 1:] + - curr_img[:-1, 1:] + - curr_img[1:, :-1] + + curr_img[:-1, :-1] + ) - return pers_img \ No newline at end of file + return pers_img diff --git a/persim/landscapes/transformer.py b/persim/landscapes/transformer.py index 60bc495..4dbe5db 100644 --- a/persim/landscapes/transformer.py +++ b/persim/landscapes/transformer.py @@ -3,11 +3,12 @@ landscapes. """ from operator import itemgetter + +import numpy as np from sklearn.base import BaseEstimator, TransformerMixin from .approximate import PersLandscapeApprox - __all__ = ["PersistenceLandscaper"] @@ -80,34 +81,35 @@ def __repr__(self): else: return f"PersistenceLandscaper(hom_deg={self.hom_deg}, start={self.start}, stop={self.stop}, num_steps={self.num_steps})" - def fit(self, dgms): + def fit(self, X: np.ndarray, y=None): """Find optimal `start` and `stop` parameters for approximating grid. Parameters ---------- - dgms : list of (-,2) numpy.ndarrays - List of persistence diagrams + X : list of (-,2) numpy.ndarrays + List of persistence diagrams. + y : Ignored + Ignored; included for sklearn compatibility. """ # TODO: remove infinities - _dgm = dgms[self.hom_deg] + _dgm = X[self.hom_deg] if self.start is None: self.start = min(_dgm, key=itemgetter(0))[0] if self.stop is None: self.stop = max(_dgm, key=itemgetter(1))[1] return self - def transform(self, dgms): + def transform(self, X: np.ndarray, y=None): """Construct persistence landscape values. Parameters ---------- - dgms : list of (-,2) numpy.ndarrays + X : list of (-,2) numpy.ndarrays List of persistence diagrams - - flatten : bool, optional - Flag determining whether output values are flattened + y : Ignored + Ignored; included for sklearn compatibility. Returns ------- @@ -116,7 +118,7 @@ def transform(self, dgms): Persistence Landscape values sampled on approximating grid. """ result = PersLandscapeApprox( - dgms=dgms, + dgms=X, start=self.start, stop=self.stop, num_steps=self.num_steps, @@ -126,8 +128,3 @@ def transform(self, dgms): return (result.values).flatten() else: return result.values - - def fit_transform(self, dgms): - self.fit(dgms=dgms) - vals = self.transform(dgms=dgms) - return vals diff --git a/test/test_distances.py b/test/test_distances.py index c66536c..48dbbd3 100644 --- a/test/test_distances.py +++ b/test/test_distances.py @@ -1,34 +1,21 @@ import numpy as np -import scipy.sparse as sps - import pytest +import scipy.sparse as sps -from persim import bottleneck, wasserstein -from persim import sliced_wasserstein -from persim import heat -from persim import gromov_hausdorff +from persim import (bottleneck, gromov_hausdorff, heat, sliced_wasserstein, + wasserstein) class TestBottleneck: def test_single(self): - d = bottleneck( - np.array([[0.5, 1]]), - np.array([[0.5, 1.1]]) - ) + d = bottleneck(np.array([[0.5, 1]]), np.array([[0.5, 1.1]])) # These are very loose bounds assert d == pytest.approx(0.1, 0.001) def test_some(self): d = bottleneck( - np.array([ - [0.5, 1], - [0.6, 1.1] - ]), - np.array([ - [0.5, 1.1], - [0.6, 1.3] - ]) + np.array([[0.5, 1], [0.6, 1.1]]), np.array([[0.5, 1.1], [0.6, 1.3]]) ) # These are very loose bounds @@ -36,62 +23,46 @@ def test_some(self): def test_diagonal(self): d = bottleneck( - np.array([ - [10.5, 10.5], - [10.6, 10.5], - [10.3, 10.3] - ]), - np.array([ - [0.5, 1.0], - [0.6, 1.2], - [0.3, 0.7] - ]) + np.array([[10.5, 10.5], [10.6, 10.5], [10.3, 10.3]]), + np.array([[0.5, 1.0], [0.6, 1.2], [0.3, 0.7]]), ) # I expect this to be 0.6 assert d == pytest.approx(0.3, 0.001) def test_different_size(self): - d = bottleneck( - np.array([ - [0.5, 1], - [0.6, 1.1] - ]), - np.array([ - [0.5, 1.1] - ]) - ) + d = bottleneck(np.array([[0.5, 1], [0.6, 1.1]]), np.array([[0.5, 1.1]])) assert d == 0.25 def test_matching(self): - dgm1 = np.array([ - [0.5, 1], - [0.6, 1.1] - ]) - dgm2 = np.array([ - [0.5, 1.1], - [0.6, 1.1], - [0.8, 1.1], - [1.0, 1.1], - ]) - - d, m = bottleneck( - dgm1, dgm2, - matching=True + dgm1 = np.array([[0.5, 1], [0.6, 1.1]]) + dgm2 = np.array( + [ + [0.5, 1.1], + [0.6, 1.1], + [0.8, 1.1], + [1.0, 1.1], + ] ) + + d, m = bottleneck(dgm1, dgm2, matching=True) u1 = np.unique(m[:, 0]) u1 = u1[u1 >= 0] u2 = np.unique(m[:, 1]) u2 = u2[u2 >= 0] assert u1.size == dgm1.shape[0] and u2.size == dgm2.shape[0] - + def test_matching_to_self(self): # Matching a diagram to itself should yield 0 - pd = np.array([[0. , 1.71858561], - [0. , 1.74160683], - [0. , 2.43430877], - [0. , 2.56949258], - [0. , np.inf]]) + pd = np.array( + [ + [0.0, 1.71858561], + [0.0, 1.74160683], + [0.0, 2.43430877], + [0.0, 2.56949258], + [0.0, np.inf], + ] + ) dist = bottleneck(pd, pd) assert dist == 0 @@ -99,55 +70,50 @@ def test_single_point_same(self): dgm = np.array([[0.11371516, 4.45734882]]) dist = bottleneck(dgm, dgm) assert dist == 0 - + def test_2x2_bisect_bug(self): dgm1 = np.array([[6, 9], [6, 8]]) dgm2 = np.array([[4, 10], [9, 10]]) dist = bottleneck(dgm1, dgm2) assert dist == 2 - + def test_one_empty(self): dgm1 = np.array([[1, 2]]) empty = np.array([[]]) dist = bottleneck(dgm1, empty) assert dist == 0.5 - + def test_inf_deathtime(self): dgm = np.array([[1, 2]]) empty = np.array([[0, np.inf]]) - with pytest.warns(UserWarning, match="dgm1 has points with non-finite death") as w: + with pytest.warns( + UserWarning, match="dgm1 has points with non-finite death" + ) as w: dist1 = bottleneck(empty, dgm) - with pytest.warns(UserWarning, match="dgm2 has points with non-finite death") as w: + with pytest.warns( + UserWarning, match="dgm2 has points with non-finite death" + ) as w: dist2 = bottleneck(dgm, empty) assert (dist1 == 0.5) and (dist2 == 0.5) def test_repeated(self): # Issue #44 - G = np.array([[ 0, 1], [0,1]]) - H = np.array([[ 0, 1]]) + G = np.array([[0, 1], [0, 1]]) + H = np.array([[0, 1]]) dist = bottleneck(G, H) assert dist == 0.5 + class TestWasserstein: def test_single(self): - d = wasserstein( - np.array([[0.5, 1]]), - np.array([[0.5, 1.1]]) - ) + d = wasserstein(np.array([[0.5, 1]]), np.array([[0.5, 1.1]])) # These are very loose bounds assert d == pytest.approx(0.1, 0.001) def test_some(self): d = wasserstein( - np.array([ - [0.6, 1.1], - [0.5, 1] - ]), - np.array([ - [0.5, 1.1], - [0.6, 1.3] - ]) + np.array([[0.6, 1.1], [0.5, 1]]), np.array([[0.5, 1.1], [0.6, 1.3]]) ) # These are very loose bounds @@ -155,10 +121,9 @@ def test_some(self): def test_matching_to_self(self): # Matching a diagram to itself should yield 0 - pd = np.array([[0. , 1.71858561], - [0. , 1.74160683], - [0. , 2.43430877], - [0. , 2.56949258]]) + pd = np.array( + [[0.0, 1.71858561], [0.0, 1.74160683], [0.0, 2.43430877], [0.0, 2.56949258]] + ) dist = wasserstein(pd, pd) assert dist == 0 @@ -166,44 +131,46 @@ def test_single_point_same(self): dgm = np.array([[0.11371516, 4.45734882]]) dist = wasserstein(dgm, dgm) assert dist == 0 - + def test_one_empty(self): dgm1 = np.array([[1, 2]]) empty = np.array([]) dist = wasserstein(dgm1, empty) - assert np.allclose(dist, np.sqrt(2)/2) + assert np.allclose(dist, np.sqrt(2) / 2) def test_inf_deathtime(self): dgm = np.array([[1, 2]]) empty = np.array([[0, np.inf]]) - with pytest.warns(UserWarning, match="dgm1 has points with non-finite death") as w: + with pytest.warns( + UserWarning, match="dgm1 has points with non-finite death" + ) as w: dist1 = wasserstein(empty, dgm) - with pytest.warns(UserWarning, match="dgm2 has points with non-finite death") as w: + with pytest.warns( + UserWarning, match="dgm2 has points with non-finite death" + ) as w: dist2 = wasserstein(dgm, empty) - assert (np.allclose(dist1, np.sqrt(2)/2)) and (np.allclose(dist2, np.sqrt(2)/2)) - + assert (np.allclose(dist1, np.sqrt(2) / 2)) and ( + np.allclose(dist2, np.sqrt(2) / 2) + ) + def test_repeated(self): - dgm1 = np.array([[0, 10], [0,10]]) + dgm1 = np.array([[0, 10], [0, 10]]) dgm2 = np.array([[0, 10]]) dist = wasserstein(dgm1, dgm2) - assert dist == 5*np.sqrt(2) + assert dist == pytest.approx(5 * np.sqrt(2)) def test_matching(self): - dgm1 = np.array([ - [0.5, 1], - [0.6, 1.1] - ]) - dgm2 = np.array([ - [0.5, 1.1], - [0.6, 1.1], - [0.8, 1.1], - [1.0, 1.1], - ]) - - d, m = wasserstein( - dgm1, dgm2, - matching=True + dgm1 = np.array([[0.5, 1], [0.6, 1.1]]) + dgm2 = np.array( + [ + [0.5, 1.1], + [0.6, 1.1], + [0.8, 1.1], + [1.0, 1.1], + ] ) + + d, m = wasserstein(dgm1, dgm2, matching=True) u1 = np.unique(m[:, 0]) u1 = u1[u1 >= 0] u2 = np.unique(m[:, 1]) @@ -213,39 +180,21 @@ def test_matching(self): class TestSliced: def test_single(self): - d = sliced_wasserstein( - np.array([[0.5, 1]]), - np.array([[0.5, 1.1]]) - ) + d = sliced_wasserstein(np.array([[0.5, 1]]), np.array([[0.5, 1.1]])) # These are very loose bounds assert d == pytest.approx(0.1, 0.01) def test_some(self): d = sliced_wasserstein( - np.array([ - [0.5, 1], - [0.6, 1.1] - ]), - np.array([ - [0.5, 1.1], - [0.6, 1.2] - ]) + np.array([[0.5, 1], [0.6, 1.1]]), np.array([[0.5, 1.1], [0.6, 1.2]]) ) # These are very loose bounds assert d == pytest.approx(0.19, 0.02) def test_different_size(self): - d = sliced_wasserstein( - np.array([ - [0.5, 1], - [0.6, 1.1] - ]), - np.array([ - [0.6, 1.2] - ]) - ) + d = sliced_wasserstein(np.array([[0.5, 1], [0.6, 1.1]]), np.array([[0.6, 1.2]])) # These are very loose bounds assert d == pytest.approx(0.314, 0.1) @@ -255,17 +204,12 @@ def test_single_point_same(self): dist = sliced_wasserstein(dgm, dgm) assert dist == 0 + class TestHeat: def test_compare(self): - """ lets at least be sure that large distances are captured """ - d1 = heat( - np.array([[0.5, 1]]), - np.array([[0.5, 1.1]]) - ) - d2 = heat( - np.array([[0.5, 1]]), - np.array([[0.5, 1.5]]) - ) + """lets at least be sure that large distances are captured""" + d1 = heat(np.array([[0.5, 1]]), np.array([[0.5, 1.1]])) + d2 = heat(np.array([[0.5, 1]]), np.array([[0.5, 1.5]])) # These are very loose bounds assert d1 < d2 @@ -278,8 +222,7 @@ def test_single_point_same(self): class TestModifiedGromovHausdorff: def test_single_point(self): - A_G = sps.csr_matrix( - ([1]*4, ([0, 0, 1, 2], [1, 3, 2, 3])), shape=(4, 4)) + A_G = sps.csr_matrix(([1] * 4, ([0, 0, 1, 2], [1, 3, 2, 3])), shape=(4, 4)) A_H = sps.csr_matrix(([], ([], [])), shape=(1, 1)) lb, ub = gromov_hausdorff(A_G, A_H) @@ -288,9 +231,11 @@ def test_single_point(self): def test_isomorphic(self): A_G = sps.csr_matrix( - ([1]*6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4)) + ([1] * 6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4) + ) A_H = sps.csr_matrix( - ([1]*6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4)) + ([1] * 6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4) + ) lb, ub = gromov_hausdorff(A_G, A_H) assert lb == 0 @@ -299,7 +244,8 @@ def test_isomorphic(self): def test_cliques(self): A_G = sps.csr_matrix(([1], ([0], [1])), shape=(2, 2)) A_H = sps.csr_matrix( - ([1]*6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4)) + ([1] * 6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4) + ) lb, ub = gromov_hausdorff(A_G, A_H) assert lb == 0.5 @@ -307,9 +253,9 @@ def test_cliques(self): def test_same_size(self): A_G = sps.csr_matrix( - ([1]*6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4)) - A_H = sps.csr_matrix( - ([1]*4, ([0, 0, 1, 2], [1, 3, 2, 3])), shape=(4, 4)) + ([1] * 6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4) + ) + A_H = sps.csr_matrix(([1] * 4, ([0, 0, 1, 2], [1, 3, 2, 3])), shape=(4, 4)) lb, ub = gromov_hausdorff(A_G, A_H) assert lb == 0.5 @@ -318,12 +264,14 @@ def test_same_size(self): def test_many_graphs(self): A_G = sps.csr_matrix(([1], ([0], [1])), shape=(2, 2)) A_H = sps.csr_matrix( - ([1]*6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4)) - A_I = sps.csr_matrix( - ([1]*4, ([0, 0, 1, 2], [1, 3, 2, 3])), shape=(4, 4)) + ([1] * 6, ([0, 0, 0, 1, 1, 2], [1, 2, 3, 2, 3, 3])), shape=(4, 4) + ) + A_I = sps.csr_matrix(([1] * 4, ([0, 0, 1, 2], [1, 3, 2, 3])), shape=(4, 4)) lbs, ubs = gromov_hausdorff([A_G, A_H, A_I]) - np.testing.assert_array_equal(lbs, np.array( - [[0, 0.5, 0.5], [0.5, 0, 0.5], [0.5, 0.5, 0]])) - np.testing.assert_array_equal(ubs, np.array( - [[0, 0.5, 0.5], [0.5, 0, 0.5], [0.5, 0.5, 0]])) + np.testing.assert_array_equal( + lbs, np.array([[0, 0.5, 0.5], [0.5, 0, 0.5], [0.5, 0.5, 0]]) + ) + np.testing.assert_array_equal( + ubs, np.array([[0, 0.5, 0.5], [0.5, 0, 0.5], [0.5, 0.5, 0]]) + ) diff --git a/test/test_landscapes.py b/test/test_landscapes.py index 6765646..1dbfd10 100644 --- a/test/test_landscapes.py +++ b/test/test_landscapes.py @@ -1,11 +1,9 @@ -import pytest import numpy as np +import pytest -from persim.landscapes import PersLandscapeExact -from persim.landscapes import PersLandscapeApprox -from persim.landscapes import PersistenceLandscaper -from persim.landscapes import vectorize, snap_pl, lc_approx, average_approx -from persim.landscapes import death_vector +from persim.landscapes import (PersistenceLandscaper, PersLandscapeApprox, + PersLandscapeExact, average_approx, + death_vector, lc_approx, snap_pl, vectorize) class TestPersLandscapeExact: @@ -14,7 +12,10 @@ def test_exact_empty(self): PersLandscapeExact() def test_exact_hom_deg(self): - P = PersLandscapeExact(dgms=[np.array([[1.0, 5.0]])], hom_deg=0,) + P = PersLandscapeExact( + dgms=[np.array([[1.0, 5.0]])], + hom_deg=0, + ) assert P.hom_deg == 0 with pytest.raises(ValueError): PersLandscapeExact(hom_deg=-1) @@ -32,35 +33,35 @@ def test_exact_critical_pairs(self): ) P.compute_landscape() + expected_P_pairs = [ + [ + [1.0, 0], + [3.0, 2.0], + [3.5, 1.5], + [5.0, 3.0], + [6.5, 1.5], + [7.0, 2.0], + [9.0, 0], + ], + [[2.0, 0], [3.5, 1.5], [5.0, 0], [6.5, 1.5], [8.0, 0]], + [[3.0, 0], [3.5, 0.5], [4.0, 0], [6.0, 0], [6.5, 0.5], [7.0, 0]], + ] + assert len(P.critical_pairs) == len(expected_P_pairs) + for idx, lambda_level in enumerate(P.critical_pairs): + assert lambda_level == expected_P_pairs[idx] + # duplicate bars Q = PersLandscapeExact(dgms=[np.array([[1, 5], [1, 5], [3, 6]])], hom_deg=0) Q.compute_landscape() - np.testing.assert_array_equal( - P.critical_pairs, - [ - [ - [1.0, 0], - [3.0, 2.0], - [3.5, 1.5], - [5.0, 3.0], - [6.5, 1.5], - [7.0, 2.0], - [9.0, 0], - ], - [[2.0, 0], [3.5, 1.5], [5.0, 0], [6.5, 1.5], [8.0, 0]], - [[3.0, 0], [3.5, 0.5], [4.0, 0], [6.0, 0], [6.5, 0.5], [7.0, 0]], - ], - ) - - np.testing.assert_array_equal( - Q.critical_pairs, - [ - [[1, 0], [3.0, 2.0], [4.0, 1.0], [4.5, 1.5], [6, 0]], - [[1, 0], [3.0, 2.0], [4.0, 1.0], [4.5, 1.5], [6, 0]], - [[3, 0], [4.0, 1.0], [5, 0]], - ], - ) + expected_Q_pairs = [ + [[1, 0], [3.0, 2.0], [4.0, 1.0], [4.5, 1.5], [6, 0]], + [[1, 0], [3.0, 2.0], [4.0, 1.0], [4.5, 1.5], [6, 0]], + [[3, 0], [4.0, 1.0], [5, 0]], + ] + assert len(Q.critical_pairs) == len(expected_Q_pairs) + for idx, lambda_level in enumerate(Q.critical_pairs): + assert lambda_level == expected_Q_pairs[idx] def test_exact_add(self): with pytest.raises(ValueError): @@ -420,7 +421,10 @@ def test_approx_get_item(self): def test_approx_norm(self): P = PersLandscapeApprox( - start=0, stop=5, num_steps=6, values=np.array([[0, 1, 1, 1, 1, 0]]), + start=0, + stop=5, + num_steps=6, + values=np.array([[0, 1, 1, 1, 1, 0]]), ) assert P.p_norm(p=2) == pytest.approx(np.sqrt(3 + (2.0 / 3.0))) assert P.sup_norm() == 1.0 @@ -450,7 +454,10 @@ def test_vectorize(self): def test_snap_PL(self): P = PersLandscapeApprox( - start=0, stop=5, num_steps=6, values=np.array([[0, 1, 1, 1, 1, 0]]), + start=0, + stop=5, + num_steps=6, + values=np.array([[0, 1, 1, 1, 1, 0]]), ) [P_snapped] = snap_pl([P], start=0, stop=10, num_steps=11) assert P_snapped.hom_deg == P.hom_deg @@ -519,7 +526,20 @@ def test_persistenceimager(self): assert pl.stop == 4.0 np.testing.assert_array_equal( pl.transform(dgms), - np.array([0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,]), + np.array( + [ + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + ] + ), ) pl2 = PersistenceLandscaper(hom_deg=1, num_steps=4) assert pl2.hom_deg == 1 @@ -530,5 +550,18 @@ def test_persistenceimager(self): pl3 = PersistenceLandscaper(hom_deg=0, num_steps=5, flatten=True) np.testing.assert_array_equal( pl3.fit_transform(dgms), - np.array([0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,]), + np.array( + [ + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + ] + ), ) diff --git a/test/test_persim.py b/test/test_persim.py index 9df7e05..2888aa1 100644 --- a/test/test_persim.py +++ b/test/test_persim.py @@ -1,6 +1,6 @@ +import numpy as np import pytest -import numpy as np from persim import PersImage @@ -13,7 +13,7 @@ def test_landscape(): def test_integer_diagrams(): - """ This test is inspired by gh issue #3 by gh user muszyna25. + """This test is inspired by gh issue #3 by gh user muszyna25. Integer diagrams return nan values. @@ -30,29 +30,27 @@ def test_integer_diagrams(): res2 = pim.transform(dgm) np.testing.assert_array_equal(res, res2) + class TestEmpty: def test_empty_diagram(self): dgm = np.zeros((0, 2)) - pim = PersImage(pixels = (10, 10)) + pim = PersImage(pixels=(10, 10)) res = pim.transform(dgm) assert np.all(res == np.zeros((10, 10))) def test_empyt_diagram_list(self): - dgm1 = [np.array([[2, 3]]), - np.zeros((0, 2))] - pim1 = PersImage(pixels = (10, 10)) + dgm1 = [np.array([[2, 3]]), np.zeros((0, 2))] + pim1 = PersImage(pixels=(10, 10)) res1 = pim1.transform(dgm1) assert np.all(res1[1] == np.zeros((10, 10))) - dgm2 = [np.zeros((0, 2)), - np.array([[2, 3]])] - pim2 = PersImage(pixels = (10, 10)) + dgm2 = [np.zeros((0, 2)), np.array([[2, 3]])] + pim2 = PersImage(pixels=(10, 10)) res2 = pim2.transform(dgm2) assert np.all(res2[0] == np.zeros((10, 10))) - dgm3 = [np.zeros((0, 2)), - np.zeros((0, 2))] - pim3 = PersImage(pixels = (10, 10)) + dgm3 = [np.zeros((0, 2)), np.zeros((0, 2))] + pim3 = PersImage(pixels=(10, 10)) res3 = pim3.transform(dgm3) assert np.all(res3[0] == np.zeros((10, 10))) assert np.all(res3[1] == np.zeros((10, 10))) @@ -75,7 +73,7 @@ def test_scales(self): assert wf([1, 0]) == 0 assert wf([1, 4]) == 1 - assert wf([1, 2]) == .5 + assert wf([1, 2]) == 0.5 class TestKernels: diff --git a/test/test_persistence_imager.py b/test/test_persistence_imager.py index 380020e..7c01336 100644 --- a/test/test_persistence_imager.py +++ b/test/test_persistence_imager.py @@ -1,63 +1,64 @@ +import numpy as np import pytest -import numpy as np -from persim import PersistenceImager -from persim import images_kernels -from persim import images_weights +from persim import PersistenceImager, images_kernels, images_weights # ---------------------------------------- # New PersistenceImager Tests # ---------------------------------------- + def test_empty_diagram(): dgm = np.zeros((0, 2)) persimgr = PersistenceImager(pixel_size=0.1) res = persimgr.transform(dgm) np.testing.assert_array_equal(res, np.zeros((10, 10))) + def test_empty_diagram_list(): - dgms1 = [np.array([[2, 3]]), - np.zeros((0, 2))] + dgms1 = [np.array([[2, 3]]), np.zeros((0, 2))] persimgr1 = PersistenceImager(pixel_size=0.1) res1 = persimgr1.transform(dgms1) - np.testing.assert_array_equal(res1[1],np.zeros((10, 10))) + np.testing.assert_array_equal(res1[1], np.zeros((10, 10))) - dgms2 = [np.zeros((0, 2)), - np.array([[2, 3]])] + dgms2 = [np.zeros((0, 2)), np.array([[2, 3]])] persimgr2 = PersistenceImager(pixel_size=0.1) res2 = persimgr2.transform(dgms2) np.testing.assert_array_equal(res2[0], np.zeros((10, 10))) - dgms3 = [np.zeros((0, 2)), - np.zeros((0, 2))] + dgms3 = [np.zeros((0, 2)), np.zeros((0, 2))] persimgr3 = PersistenceImager(pixel_size=0.1) res3 = persimgr3.transform(dgms3) np.testing.assert_array_equal(res3[0], np.zeros((10, 10))) np.testing.assert_array_equal(res3[1], np.zeros((10, 10))) + def test_birth_range_setter(): - persimgr = PersistenceImager(birth_range=(0,1), pers_range=(0,2), pixel_size=1) + persimgr = PersistenceImager(birth_range=(0, 1), pers_range=(0, 2), pixel_size=1) persimgr.birth_range = (0.0, 4.5) - + np.testing.assert_equal(persimgr.pixel_size, 1) np.testing.assert_equal(persimgr._pixel_size, 1) - np.testing.assert_equal(persimgr.pers_range, (0,2)) - np.testing.assert_equal(persimgr._pers_range, (0,2)) - np.testing.assert_equal(persimgr.birth_range, (-.25, 4.75)) - np.testing.assert_equal(persimgr._birth_range, (-.25, 4.75)) + np.testing.assert_equal(persimgr.pers_range, (0, 2)) + np.testing.assert_equal(persimgr._pers_range, (0, 2)) + np.testing.assert_equal(persimgr.birth_range, (-0.25, 4.75)) + np.testing.assert_equal(persimgr._birth_range, (-0.25, 4.75)) np.testing.assert_equal(persimgr.width, 5) np.testing.assert_equal(persimgr._width, 5) np.testing.assert_equal(persimgr.height, 2) np.testing.assert_equal(persimgr._height, 2) np.testing.assert_equal(persimgr.resolution, (5, 2)) np.testing.assert_equal(persimgr._resolution, (5, 2)) - np.testing.assert_array_equal(persimgr._bpnts, [-0.25, 0.75, 1.75, 2.75, 3.75, 4.75]) - np.testing.assert_array_equal(persimgr._ppnts, [0., 1., 2.]) - + np.testing.assert_array_equal( + persimgr._bpnts, [-0.25, 0.75, 1.75, 2.75, 3.75, 4.75] + ) + np.testing.assert_array_equal(persimgr._ppnts, [0.0, 1.0, 2.0]) + + def test_pers_range_setter(): - persimgr = PersistenceImager(birth_range=(0,1), pers_range=(0,2), pixel_size=1) + persimgr = PersistenceImager(birth_range=(0, 1), pers_range=(0, 2), pixel_size=1) persimgr.pers_range = (-1.5, 4.5) - + np.testing.assert_equal(persimgr.pixel_size, 1) np.testing.assert_equal(persimgr._pixel_size, 1) np.testing.assert_equal(persimgr.pers_range, (-1.5, 4.5)) @@ -70,15 +71,18 @@ def test_pers_range_setter(): np.testing.assert_equal(persimgr._height, 6) np.testing.assert_equal(persimgr.resolution, (1, 6)) np.testing.assert_equal(persimgr._resolution, (1, 6)) - np.testing.assert_array_equal(persimgr._ppnts, [-1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5]) - np.testing.assert_array_equal(persimgr._bpnts, [0., 1.]) + np.testing.assert_array_equal( + persimgr._ppnts, [-1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + ) + np.testing.assert_array_equal(persimgr._bpnts, [0.0, 1.0]) + def test_pixel_size_setter(): - persimgr = PersistenceImager(birth_range=(0,1), pers_range=(0,2), pixel_size=1) - persimgr.pixel_size = .75 - - np.testing.assert_equal(persimgr.pixel_size, .75) - np.testing.assert_equal(persimgr._pixel_size, .75) + persimgr = PersistenceImager(birth_range=(0, 1), pers_range=(0, 2), pixel_size=1) + persimgr.pixel_size = 0.75 + + np.testing.assert_equal(persimgr.pixel_size, 0.75) + np.testing.assert_equal(persimgr._pixel_size, 0.75) np.testing.assert_equal(persimgr.birth_range, (-0.25, 1.25)) np.testing.assert_equal(persimgr._birth_range, (-0.25, 1.25)) np.testing.assert_equal(persimgr.pers_range, (-0.125, 2.125)) @@ -87,17 +91,17 @@ def test_pixel_size_setter(): np.testing.assert_equal(persimgr._height, 2.25) np.testing.assert_equal(persimgr.width, 1.5) np.testing.assert_equal(persimgr._width, 1.5) - np.testing.assert_equal(persimgr.resolution, (2,3)) - np.testing.assert_equal(persimgr._resolution, (2,3)) - np.testing.assert_array_equal(persimgr._ppnts, [-0.125, 0.625, 1.375, 2.125]) - np.testing.assert_array_equal(persimgr._bpnts, [-0.25, 0.5 , 1.25]) - + np.testing.assert_equal(persimgr.resolution, (2, 3)) + np.testing.assert_equal(persimgr._resolution, (2, 3)) + np.testing.assert_array_equal(persimgr._ppnts, [-0.125, 0.625, 1.375, 2.125]) + np.testing.assert_array_equal(persimgr._bpnts, [-0.25, 0.5, 1.25]) + def test_fit_diagram(): - persimgr = PersistenceImager(birth_range=(0,1), pers_range=(0,2), pixel_size=1) - dgm = np.array([[1,2],[4,8],[-1,5.25]]) + persimgr = PersistenceImager(birth_range=(0, 1), pers_range=(0, 2), pixel_size=1) + dgm = np.array([[1, 2], [4, 8], [-1, 5.25]]) persimgr.fit(dgm) - + np.testing.assert_equal(persimgr.pixel_size, 1) np.testing.assert_equal(persimgr._pixel_size, 1) np.testing.assert_equal(persimgr.birth_range, (-1, 4)) @@ -108,17 +112,19 @@ def test_fit_diagram(): np.testing.assert_equal(persimgr._height, 6) np.testing.assert_equal(persimgr.width, 5) np.testing.assert_equal(persimgr._width, 5) - np.testing.assert_equal(persimgr.resolution, (5,6)) - np.testing.assert_equal(persimgr._resolution, (5,6)) - np.testing.assert_array_equal(persimgr._ppnts, [0.625, 1.625, 2.625, 3.625, 4.625, 5.625, 6.625]) - np.testing.assert_array_equal(persimgr._bpnts, [-1., 0., 1., 2., 3., 4.]) + np.testing.assert_equal(persimgr.resolution, (5, 6)) + np.testing.assert_equal(persimgr._resolution, (5, 6)) + np.testing.assert_array_equal( + persimgr._ppnts, [0.625, 1.625, 2.625, 3.625, 4.625, 5.625, 6.625] + ) + np.testing.assert_array_equal(persimgr._bpnts, [-1.0, 0.0, 1.0, 2.0, 3.0, 4.0]) + - def test_fit_diagram_list(): - persimgr = PersistenceImager(birth_range=(0,1), pers_range=(0,2), pixel_size=1) - dgms = [np.array([[1,2],[4,8],[-1,5.25]]), np.array([[1,2],[2,3],[3,4]])] + persimgr = PersistenceImager(birth_range=(0, 1), pers_range=(0, 2), pixel_size=1) + dgms = [np.array([[1, 2], [4, 8], [-1, 5.25]]), np.array([[1, 2], [2, 3], [3, 4]])] persimgr.fit(dgms) - + np.testing.assert_equal(persimgr.pixel_size, 1) np.testing.assert_equal(persimgr._pixel_size, 1) np.testing.assert_equal(persimgr.birth_range, (-1, 4)) @@ -129,163 +135,231 @@ def test_fit_diagram_list(): np.testing.assert_equal(persimgr._height, 6) np.testing.assert_equal(persimgr.width, 5) np.testing.assert_equal(persimgr._width, 5) - np.testing.assert_equal(persimgr.resolution, (5,6)) - np.testing.assert_equal(persimgr._resolution, (5,6)) - np.testing.assert_array_equal(persimgr._ppnts, [0.625, 1.625, 2.625, 3.625, 4.625, 5.625, 6.625]) - np.testing.assert_array_equal(persimgr._bpnts, [-1., 0., 1., 2., 3., 4.]) + np.testing.assert_equal(persimgr.resolution, (5, 6)) + np.testing.assert_equal(persimgr._resolution, (5, 6)) + np.testing.assert_array_equal( + persimgr._ppnts, [0.625, 1.625, 2.625, 3.625, 4.625, 5.625, 6.625] + ) + np.testing.assert_array_equal(persimgr._bpnts, [-1.0, 0.0, 1.0, 2.0, 3.0, 4.0]) + def test_mixed_pairs(): - """ This test is inspired by gh issue #3 by gh user muszyna25. + """This test is inspired by gh issue #3 by gh user muszyna25. Integer diagrams return nan values. This does not work: dgm = [[0, 2], [0, 6], [0, 8]]; This one works fine: dgm = [[0.0, 2.0], [0.0, 6.0], [0.0, 8.0]]; """ persimgr = PersistenceImager() - + dgm = [[0, 2], [0, 6], [0, 8]] dgm2 = [[0.0, 2.0], [0.0, 6.0], [0.0, 8.0]] dgm3 = [[0.0, 2], [0.0, 6.0], [0, 8.0e0]] - + res = persimgr.transform(dgm) res2 = persimgr.transform(dgm2) res3 = persimgr.transform(dgm3) - + np.testing.assert_array_equal(res, res2) np.testing.assert_array_equal(res, res3) + def test_parameter_exceptions(): def construct_imager(param_dict): pimgr = PersistenceImager(**param_dict) - np.testing.assert_raises(ValueError, construct_imager, {'birth_range': 0}) - np.testing.assert_raises(ValueError, construct_imager, {'birth_range': ('str', 0)}) - np.testing.assert_raises(ValueError, construct_imager, {'birth_range': (0, 0, 0)}) - np.testing.assert_raises(ValueError, construct_imager, {'pers_range': 0}) - np.testing.assert_raises(ValueError, construct_imager, {'pers_range': ('str', 0)}) - np.testing.assert_raises(ValueError, construct_imager, {'pers_range': (0, 0, 0)}) - np.testing.assert_raises(ValueError, construct_imager, {'pixel_size': 'str'}) - np.testing.assert_raises(ValueError, construct_imager, {'weight': 0}) - np.testing.assert_raises(ValueError, construct_imager, {'weight': 'invalid_weight'}) - np.testing.assert_raises(ValueError, construct_imager, {'kernel': 0}) - np.testing.assert_raises(ValueError, construct_imager, {'kernel': 'invalid_kernel'}) - np.testing.assert_raises(ValueError, construct_imager, {'weight_params': 0}) - np.testing.assert_raises(ValueError, construct_imager, {'kernel_params': 0}) + np.testing.assert_raises(ValueError, construct_imager, {"birth_range": 0}) + np.testing.assert_raises(ValueError, construct_imager, {"birth_range": ("str", 0)}) + np.testing.assert_raises(ValueError, construct_imager, {"birth_range": (0, 0, 0)}) + np.testing.assert_raises(ValueError, construct_imager, {"pers_range": 0}) + np.testing.assert_raises(ValueError, construct_imager, {"pers_range": ("str", 0)}) + np.testing.assert_raises(ValueError, construct_imager, {"pers_range": (0, 0, 0)}) + np.testing.assert_raises(ValueError, construct_imager, {"pixel_size": "str"}) + np.testing.assert_raises(ValueError, construct_imager, {"weight": 0}) + np.testing.assert_raises(ValueError, construct_imager, {"weight": "invalid_weight"}) + np.testing.assert_raises(ValueError, construct_imager, {"kernel": 0}) + np.testing.assert_raises(ValueError, construct_imager, {"kernel": "invalid_kernel"}) + np.testing.assert_raises(ValueError, construct_imager, {"weight_params": 0}) + np.testing.assert_raises(ValueError, construct_imager, {"kernel_params": 0}) + class TestWeightFunctions: def test_zero_on_birthaxis(self): - persimgr = PersistenceImager(weight=images_weights.linear_ramp, weight_params={'low':0.0, 'high':1.0, 'start':0.0, 'end':1.0}) + persimgr = PersistenceImager( + weight=images_weights.linear_ramp, + weight_params={"low": 0.0, "high": 1.0, "start": 0.0, "end": 1.0}, + ) wf = persimgr.weight wf_params = persimgr.weight_params np.testing.assert_equal(wf(1, 0, **wf_params), 0) - - persimgr = PersistenceImager(weight=images_weights.persistence, weight_params={'n': 2}) + + persimgr = PersistenceImager( + weight=images_weights.persistence, weight_params={"n": 2} + ) wf = persimgr.weight wf_params = persimgr.weight_params np.testing.assert_equal(wf(1, 0, **wf_params), 0) def test_linear_ramp(self): - persimgr = PersistenceImager(weight=images_weights.linear_ramp, weight_params={'low':0.0, 'high':5.0, 'start':0.0, 'end':1.0}) + persimgr = PersistenceImager( + weight=images_weights.linear_ramp, + weight_params={"low": 0.0, "high": 5.0, "start": 0.0, "end": 1.0}, + ) wf = persimgr.weight wf_params = persimgr.weight_params np.testing.assert_equal(wf(1, 0, **wf_params), 0) - np.testing.assert_equal(wf(1, 1/5, **wf_params), 1) + np.testing.assert_equal(wf(1, 1 / 5, **wf_params), 1) np.testing.assert_equal(wf(1, 1, **wf_params), 5) np.testing.assert_equal(wf(1, 2, **wf_params), 5) - - persimgr.weight_params = {'low':0.0, 'high':5.0, 'start':0.0, 'end':5.0} + + persimgr.weight_params = {"low": 0.0, "high": 5.0, "start": 0.0, "end": 5.0} wf_params = persimgr.weight_params - + np.testing.assert_equal(wf(1, 0, **wf_params), 0) - np.testing.assert_equal(wf(1, 1/5, **wf_params), 1/5) + np.testing.assert_equal(wf(1, 1 / 5, **wf_params), 1 / 5) np.testing.assert_equal(wf(1, 1, **wf_params), 1) np.testing.assert_equal(wf(1, 5, **wf_params), 5) - - persimgr.weight_params = {'low':0.0, 'high':5.0, 'start':1.0, 'end':5.0} + + persimgr.weight_params = {"low": 0.0, "high": 5.0, "start": 1.0, "end": 5.0} wf_params = persimgr.weight_params - + np.testing.assert_equal(wf(1, 0, **wf_params), 0) np.testing.assert_equal(wf(1, 1, **wf_params), 0) np.testing.assert_equal(wf(1, 5, **wf_params), 5) - - persimgr.weight_params = {'low':1.0, 'high':5.0, 'start':1.0, 'end':5.0} - wf_params = persimgr.weight_params + + persimgr.weight_params = {"low": 1.0, "high": 5.0, "start": 1.0, "end": 5.0} + wf_params = persimgr.weight_params np.testing.assert_equal(wf(1, 0, **wf_params), 1) np.testing.assert_equal(wf(1, 1, **wf_params), 1) np.testing.assert_equal(wf(1, 2, **wf_params), 2) def test_persistence(self): - persimgr = PersistenceImager(weight=images_weights.persistence, weight_params={'n':1.0}) + persimgr = PersistenceImager( + weight=images_weights.persistence, weight_params={"n": 1.0} + ) wf = persimgr.weight wf_params = persimgr.weight_params - + x = np.random.rand() np.testing.assert_equal(wf(1, x, **wf_params), x) - - persimgr.weight_params = {'n':1.5} + + persimgr.weight_params = {"n": 1.5} wf_params = persimgr.weight_params - - np.testing.assert_equal(wf(1, x, **wf_params), x ** 1.5) - + + np.testing.assert_equal(wf(1, x, **wf_params), x**1.5) + + class TestKernelFunctions: def test_gaussian(self): kernel = images_kernels.gaussian - kernel_params = {'mu':[1, 1], 'sigma':np.array([[1,0],[0,1]])} - np.testing.assert_almost_equal(kernel(np.array([1]), np.array([1]), **kernel_params), 1/4, 8) - + kernel_params = {"mu": [1, 1], "sigma": np.array([[1, 0], [0, 1]])} + np.testing.assert_almost_equal( + kernel(np.array([1]), np.array([1]), **kernel_params), 1 / 4, 8 + ) + kernel = images_kernels.bvn_cdf - kernel_params = {'mu_x':1.0, 'mu_y':1.0, 'sigma_xx':1.0, 'sigma_yy':1.0, 'sigma_xy':0.0} - np.testing.assert_almost_equal(kernel(np.array([1]), np.array([1]), **kernel_params), 1/4, 8) - - kernel_params = {'mu_x':1.0, 'mu_y':1.0, 'sigma_xx':1.0, 'sigma_yy':1.0, 'sigma_xy':0.5} - np.testing.assert_almost_equal(kernel(np.array([1]), np.array([1]), **kernel_params), 1/3, 8) - - kernel_params = {'mu_x':1.0, 'mu_y':1.0, 'sigma_xx':1.0, 'sigma_yy':2.0, 'sigma_xy':0.0} - np.testing.assert_almost_equal(kernel(np.array([1]), np.array([0]), **kernel_params), 0.11987503, 8) - - kernel_params = {'mu_x':1.0, 'mu_y':1.0, 'sigma_xx':1.0, 'sigma_yy':2.0, 'sigma_xy':1.0} - np.testing.assert_equal(kernel(np.array([1]), np.array([1]), **kernel_params), .375) - + kernel_params = { + "mu_x": 1.0, + "mu_y": 1.0, + "sigma_xx": 1.0, + "sigma_yy": 1.0, + "sigma_xy": 0.0, + } + np.testing.assert_almost_equal( + kernel(np.array([1]), np.array([1]), **kernel_params), 1 / 4, 8 + ) + + kernel_params = { + "mu_x": 1.0, + "mu_y": 1.0, + "sigma_xx": 1.0, + "sigma_yy": 1.0, + "sigma_xy": 0.5, + } + np.testing.assert_almost_equal( + kernel(np.array([1]), np.array([1]), **kernel_params), 1 / 3, 8 + ) + + kernel_params = { + "mu_x": 1.0, + "mu_y": 1.0, + "sigma_xx": 1.0, + "sigma_yy": 2.0, + "sigma_xy": 0.0, + } + np.testing.assert_almost_equal( + kernel(np.array([1]), np.array([0]), **kernel_params), 0.11987503, 8 + ) + + kernel_params = { + "mu_x": 1.0, + "mu_y": 1.0, + "sigma_xx": 1.0, + "sigma_yy": 2.0, + "sigma_xy": 1.0, + } + np.testing.assert_equal( + kernel(np.array([1]), np.array([1]), **kernel_params), 0.375 + ) + def test_norm_cdf(self): - np.testing.assert_equal(images_kernels.norm_cdf(0), .5) - np.testing.assert_almost_equal(images_kernels.norm_cdf(1), 0.8413447460685429, 8) - + np.testing.assert_equal(images_kernels.norm_cdf(0), 0.5) + np.testing.assert_almost_equal( + images_kernels.norm_cdf(1), 0.8413447460685429, 8 + ) + def test_uniform(self): kernel = images_kernels.uniform - kernel_params={'width': 3, 'height': 1} + kernel_params = {"width": 3, "height": 1} - np.testing.assert_equal(kernel(np.array([-1]), np.array([-1]), mu=(0,0), **kernel_params), 0) - np.testing.assert_equal(kernel(np.array([3]), np.array([1]), mu=(0,0), **kernel_params), 1) - np.testing.assert_equal(kernel(np.array([5]), np.array([5]), mu=(0,0), **kernel_params), 1) + np.testing.assert_equal( + kernel(np.array([-1]), np.array([-1]), mu=(0, 0), **kernel_params), 0 + ) + np.testing.assert_equal( + kernel(np.array([3]), np.array([1]), mu=(0, 0), **kernel_params), 1 + ) + np.testing.assert_equal( + kernel(np.array([5]), np.array([5]), mu=(0, 0), **kernel_params), 1 + ) def test_sigma(self): kernel = images_kernels.gaussian - kernel_params1 = {'sigma':np.array([[1,0],[0,1]])} - kernel_params2 = {'sigma': [[1,0],[0,1]]} - np.testing.assert_equal(kernel(np.array([1]), np.array([1]), **kernel_params1), kernel(np.array([1]), np.array([1]), **kernel_params2)) - + kernel_params1 = {"sigma": np.array([[1, 0], [0, 1]])} + kernel_params2 = {"sigma": [[1, 0], [0, 1]]} + np.testing.assert_equal( + kernel(np.array([1]), np.array([1]), **kernel_params1), + kernel(np.array([1]), np.array([1]), **kernel_params2), + ) + + class TestTransformOutput: def test_lists_of_lists(self): - persimgr = PersistenceImager(birth_range=(0,3), pers_range=(0,3), pixel_size=1) + persimgr = PersistenceImager( + birth_range=(0, 3), pers_range=(0, 3), pixel_size=1 + ) dgm = [[0, 1], [1, 1], [3, 5]] img = persimgr.transform(dgm) np.testing.assert_equal(img.shape, (3, 3)) - + def test_n_pixels(self): - persimgr = PersistenceImager(birth_range=(0,5), pers_range=(0,3), pixel_size=1) + persimgr = PersistenceImager( + birth_range=(0, 5), pers_range=(0, 3), pixel_size=1 + ) dgm = np.array([[0, 1], [1, 1], [3, 5]]) img = persimgr.transform(dgm) np.testing.assert_equal(img.shape, (5, 3)) - + img = persimgr.fit_transform(dgm) np.testing.assert_equal(img.shape, (3, 2)) def test_multiple_diagrams(self): - persimgr = PersistenceImager(birth_range=(0,5), pers_range=(0,3), pixel_size=1) + persimgr = PersistenceImager( + birth_range=(0, 5), pers_range=(0, 3), pixel_size=1 + ) dgm1 = np.array([[0, 1], [1, 1], [3, 5]]) dgm2 = np.array([[0, 1], [1, 1], [3, 6], [1, 1]]) @@ -293,8 +367,8 @@ def test_multiple_diagrams(self): np.testing.assert_equal(len(imgs), 2) np.testing.assert_equal(imgs[0].shape, imgs[1].shape) - + imgs = persimgr.fit_transform([dgm1, dgm2]) np.testing.assert_equal(len(imgs), 2) np.testing.assert_equal(imgs[0].shape, imgs[1].shape) - np.testing.assert_equal(imgs[0].shape, (3, 3)) \ No newline at end of file + np.testing.assert_equal(imgs[0].shape, (3, 3))