Source code for cechmate.filtrations.alpha
import itertools
import time
import warnings
import numpy as np
from numpy import linalg
from scipy import spatial
from .base import BaseFiltration
__all__ = ["Alpha"]
[docs]class Alpha(BaseFiltration):
""" Construct an Alpha filtration from the given data.
Note
=====
Alpha filtrations use radius instead of diameter. Multiply results or X by 2 when comparing the filtration to Rips or Cech.
Examples
========
>>> r = Alpha()
>>> simplices = r.build(X)
>>> diagrams = r.diagrams(simplices)
"""
MIN_DET = 1e-10
def build(self, X):
"""
Do the Alpha filtration of a Euclidean point set (requires scipy)
Parameters
===========
X: Nxd array
Array of N Euclidean vectors in d dimensions
"""
if X.shape[0] < X.shape[1]:
warnings.warn(
"The input point cloud has more columns than rows; "
+ "did you mean to transpose?"
)
maxdim = self.maxdim
if not self.maxdim:
maxdim = X.shape[1] - 1
## Step 1: Figure out the filtration
if self.verbose:
print("Doing spatial.Delaunay triangulation...")
tic = time.time()
delaunay_faces = spatial.Delaunay(X).simplices
if self.verbose:
print(
"Finished spatial.Delaunay triangulation (Elapsed Time %.3g)"
% (time.time() - tic)
)
print("Building alpha filtration...")
tic = time.time()
filtration = {}
for dim in range(maxdim + 2, 1, -1):
for s in range(delaunay_faces.shape[0]):
simplex = delaunay_faces[s, :]
for sigma in itertools.combinations(simplex, dim):
sigma = tuple(sorted(sigma))
if not sigma in filtration:
rSqr = self._get_circumcenter(X[sigma, :])[1]
if np.isfinite(rSqr):
filtration[sigma] = rSqr
if sigma in filtration:
for i in range(dim): # Propagate alpha filtration value
tau = sigma[0:i] + sigma[i + 1 : :]
if tau in filtration:
filtration[tau] = min(
filtration[tau], filtration[sigma]
)
elif len(tau) > 1 and sigma in filtration:
# If Tau is not empty
xtau, rtauSqr = self._get_circumcenter(X[tau, :])
if np.sum((X[sigma[i], :] - xtau) ** 2) < rtauSqr:
filtration[tau] = filtration[sigma]
# Convert from squared radii to radii
for sigma in filtration:
filtration[sigma] = np.sqrt(filtration[sigma])
## Step 2: Take care of numerical artifacts that may result
## in simplices with greater filtration values than their co-faces
simplices_bydim = [set([]) for i in range(maxdim + 2)]
for simplex in filtration.keys():
simplices_bydim[len(simplex) - 1].add(simplex)
simplices_bydim = simplices_bydim[2::]
simplices_bydim.reverse()
for simplices_dim in simplices_bydim:
for sigma in simplices_dim:
for i in range(len(sigma)):
tau = sigma[0:i] + sigma[i + 1 : :]
if filtration[tau] > filtration[sigma]:
filtration[tau] = filtration[sigma]
if self.verbose:
print(
"Finished building alpha filtration (Elapsed Time %.3g)"
% (time.time() - tic)
)
simplices = [([i], 0) for i in range(X.shape[0])]
simplices.extend(filtration.items())
self.simplices_ = simplices
return simplices
def _get_circumcenter(self, X):
"""
Compute the circumcenter and circumradius of a simplex
Parameters
----------
X : ndarray (N, d)
Coordinates of points on an N-simplex in d dimensions
Returns
-------
(circumcenter, circumradius)
A tuple of the circumcenter and squared circumradius.
(SC1) If there are fewer points than the ambient dimension plus one,
then return the circumcenter corresponding to the smallest
possible squared circumradius
(SC2) If the points are not in general position,
it returns (np.inf, np.inf)
(SC3) If there are more points than the ambient dimension plus one
it returns (np.nan, np.nan)
"""
X0 = np.array(X)
if X.shape[0] == 2:
# Special case of an edge, which is very simple
dX = X[1, :] - X[0, :]
rSqr = 0.25 * np.sum(dX ** 2)
x = X[0, :] + 0.5 * dX
return (x, rSqr)
if X.shape[0] > X.shape[1] + 1: # SC3 (too many points)
warnings.warn(
"Trying to compute circumsphere for "
+ "%i points in %i dimensions" % (X.shape[0], X.shape[1])
)
return (np.nan, np.nan)
# Transform arrays for PCA for SC1 (points in higher ambient dimension)
muV = np.array([])
V = np.array([])
if X.shape[0] < X.shape[1] + 1: # SC1: Do PCA down to NPoints-1
muV = np.mean(X, 0)
XCenter = X - muV
_, V = linalg.eigh((XCenter.T).dot(XCenter))
V = V[:, (X.shape[1] - X.shape[0] + 1) : :] # Put dimension NPoints-1
X = XCenter.dot(V)
muX = np.mean(X, 0)
D = np.ones((X.shape[0], X.shape[0] + 1))
# Subtract off centroid and scale down for numerical stability
Y = X - muX
scaleSqr = np.max(np.sum(Y ** 2, 1))
scaleSqr = 1
scale = np.sqrt(scaleSqr)
Y /= scale
D[:, 1:-1] = Y
D[:, 0] = np.sum(D[:, 1:-1] ** 2, 1)
minor = lambda A, j: A[
:, np.concatenate((np.arange(j), np.arange(j + 1, A.shape[1])))
]
dxs = np.array([linalg.det(minor(D, i)) for i in range(1, D.shape[1] - 1)])
alpha = linalg.det(minor(D, 0))
if np.abs(alpha) > Alpha.MIN_DET:
signs = (-1) ** np.arange(len(dxs))
x = dxs * signs / (2 * alpha) + muX # Add back centroid
gamma = ((-1) ** len(dxs)) * linalg.det(minor(D, D.shape[1] - 1))
rSqr = (np.sum(dxs ** 2) + 4 * alpha * gamma) / (4 * alpha * alpha)
x *= scale
rSqr *= scaleSqr
if V.size > 0:
# Transform back to ambient if SC1
x = x.dot(V.T) + muV
return (x, rSqr)
return (np.inf, np.inf) # SC2 (Points not in general position)