"""
Implementation of the bottleneck distance using binary
search and the Hopcroft-Karp algorithm
Author: Chris Tralie
"""
import numpy as np
from bisect import bisect_left
from hopcroftkarp import HopcroftKarp
import warnings
__all__ = ["bottleneck"]
[docs]
def bottleneck(dgm1, dgm2, matching=False):
"""
Perform the Bottleneck distance matching between persistence diagrams.
Assumes first two columns of S and T are the coordinates of the persistence
points, but allows for other coordinate columns (which are ignored in
diagonal matching).
See the `distances` notebook for an example of how to use this.
Parameters
-----------
dgm1: Mx(>=2)
array of birth/death pairs for PD 1
dgm2: Nx(>=2)
array of birth/death paris for PD 2
matching: bool, default False
if True, return matching infromation and cross-similarity matrix
Returns
--------
d: float
bottleneck distance between dgm1 and dgm2
matching: ndarray(Mx+Nx, 3), Only returns if `matching=True`
A list of correspondences in an optimal matching, as well as their distance, where:
* First column is index of point in first persistence diagram, or -1 if diagonal
* Second column is index of point in second persistence diagram, or -1 if diagonal
* Third column is the distance of each matching
"""
return_matching = matching
S = np.array(dgm1)
M = min(S.shape[0], S.size)
if S.size > 0:
S = S[np.isfinite(S[:, 1]), :]
if S.shape[0] < M:
warnings.warn(
"dgm1 has points with non-finite death times;" + "ignoring those points"
)
M = S.shape[0]
T = np.array(dgm2)
N = min(T.shape[0], T.size)
if T.size > 0:
T = T[np.isfinite(T[:, 1]), :]
if T.shape[0] < N:
warnings.warn(
"dgm2 has points with non-finite death times;" + "ignoring those points"
)
N = T.shape[0]
if M == 0:
S = np.array([[0, 0]])
M = 1
if N == 0:
T = np.array([[0, 0]])
N = 1
# Step 1: Compute CSM between S and T, including points on diagonal
# L Infinity distance
Sb, Sd = S[:, 0], S[:, 1]
Tb, Td = T[:, 0], T[:, 1]
D1 = np.abs(Sb[:, None] - Tb[None, :])
D2 = np.abs(Sd[:, None] - Td[None, :])
DUL = np.maximum(D1, D2)
# Put diagonal elements into the matrix, being mindful that Linfinity
# balls meet the diagonal line at a diamond vertex
D = np.zeros((M + N, M + N))
# Upper left is Linfinity cross-similarity between two diagrams
D[0:M, 0:N] = DUL
# Upper right is diagonal matching of points from S
UR = np.inf * np.ones((M, M))
np.fill_diagonal(UR, 0.5 * (S[:, 1] - S[:, 0]))
D[0:M, N::] = UR
# Lower left is diagonal matching of points from T
UL = np.inf * np.ones((N, N))
np.fill_diagonal(UL, 0.5 * (T[:, 1] - T[:, 0]))
D[M::, 0:N] = UL
# Lower right is all 0s by default (remaining diagonals match to diagonals)
# Step 2: Perform a binary search + Hopcroft Karp to find the
# bottleneck distance
ds = np.sort(np.unique(D.flatten())) # [0:-1] # Everything but np.inf
bdist = ds[-1]
matching = {}
while len(ds) >= 1:
idx = 0
if len(ds) > 1:
idx = bisect_left(range(ds.size), int(ds.size / 2))
d = ds[idx]
graph = {}
for i in range(D.shape[0]):
graph["{}".format(i)] = {j for j in range(D.shape[1]) if D[i, j] <= d}
res = HopcroftKarp(graph).maximum_matching()
if len(res) == 2 * D.shape[0] and d <= bdist:
bdist = d
matching = res
ds = ds[0:idx]
else:
ds = ds[idx + 1 : :]
if return_matching:
matchidx = []
for i in range(M + N):
j = matching["{}".format(i)]
d = D[i, j]
if i < M:
if j >= N:
j = -1 # Diagonal match from first persistence diagram
else:
if j >= N: # Diagonal to diagonal, so don't include this
continue
i = -1
matchidx.append([i, j, d])
return bdist, np.array(matchidx)
else:
return bdist