├── README.md ├── .gitignore ├── tda ├── __init__.py ├── matreduce.py ├── plotting.py ├── betti.py ├── snf.pyx ├── SimplicialComplex.py ├── FilteredSimplicialComplex.py └── persistent_homology.py ├── setup.py └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # OpenTDA 2 | Open source Python library for topological data analysis (TDA) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build/ 3 | dist/ 4 | .ipynb_checkpoints/ 5 | *.egg-info/ 6 | *.a 7 | *.ipynb 8 | *.so 9 | .DS_Store 10 | *.c 11 | -------------------------------------------------------------------------------- /tda/__init__.py: -------------------------------------------------------------------------------- 1 | from .persistent_homology import PersistentHomology 2 | 3 | __name__ = 'OpenTDA' 4 | __version__ = 0.1 5 | 6 | __all__ = ['PersistentHomology'] 7 | -------------------------------------------------------------------------------- /tda/matreduce.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.linalg import svd 3 | 4 | 5 | def reduce_matrix(A, eps=None): 6 | if np.size(A) == 0: 7 | return A, 0, 0 8 | if np.size(A) == 1: 9 | return A, 1, [] 10 | 11 | m, n = A.shape 12 | if m != n: 13 | M = np.zeros(2 * (max(A.shape), )) 14 | M[:m, :n] = A 15 | else: 16 | M = A 17 | 18 | u, s, v = svd(M) 19 | if eps is None: 20 | eps = s.max() * max(M.shape) * np.finfo(s.dtype).eps 21 | 22 | null_mask = (s <= eps) 23 | 24 | rank = sum(~null_mask) 25 | null_space = v[null_mask] 26 | 27 | u = u[~null_mask][:, ~null_mask] 28 | s = np.diag(s[~null_mask]) 29 | v = v[~null_mask] 30 | reduced = u.dot(s.dot(v)) 31 | 32 | return reduced, rank, null_space 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """OpenTDA: Open-source Python library for topological data analysis""" 2 | import sys 3 | import numpy as np 4 | from os.path import join as pjoin 5 | from setuptools import setup, Extension, find_packages 6 | from tda import __name__, __version__ 7 | 8 | try: 9 | import Cython 10 | from Cython.Distutils import build_ext 11 | 12 | if Cython.__version__ < '0.18': 13 | raise ImportError() 14 | except ImportError: 15 | print( 16 | 'Cython version 0.18 or later is required. Try "conda install cython"') 17 | sys.exit(1) 18 | 19 | NAME = __name__ 20 | VERSION = __version__ 21 | 22 | 23 | def read(filename): 24 | import os 25 | BASE_DIR = os.path.dirname(__file__) 26 | filename = os.path.join(BASE_DIR, filename) 27 | with open(filename, 'r') as fi: 28 | return fi.read() 29 | 30 | 31 | def readlist(filename): 32 | rows = read(filename).split("\n") 33 | rows = [x.strip() for x in rows if x.strip()] 34 | return list(rows) 35 | 36 | 37 | extensions = [] 38 | extensions.append( 39 | Extension('tda.snf', 40 | sources=[pjoin('tda', 'snf.pyx')], 41 | include_dirs=[np.get_include()])) 42 | 43 | 44 | setup( 45 | name=NAME, 46 | version=VERSION, 47 | description=('Open-source Python library for topological data analysis (TDA).'), 48 | author="Brandon B", 49 | author_email="outlacedev@gmail.com", 50 | url='https://github.com/outlace/%s' % NAME, 51 | download_url='https://github.com/outlace/%s/tarball/master' % NAME, 52 | license='ALv2', 53 | packages=find_packages(), 54 | package_data={ 55 | '': ['README.md'], 56 | }, 57 | zip_safe=False, 58 | ext_modules=extensions, 59 | cmdclass={'build_ext': build_ext} 60 | ) 61 | -------------------------------------------------------------------------------- /tda/plotting.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | 3 | 4 | def drawComplex(data, ph, axes=[-6, 8, -6, 6]): 5 | plt.clf() 6 | plt.axis(axes) # axes = [x1, x2, y1, y2] 7 | plt.scatter(data[:, 0], data[:, 1]) # plotting just for clarity 8 | for i, txt in enumerate(data): 9 | plt.annotate(i, (data[i][0] + 0.05, data[i][1])) # add labels 10 | 11 | # add lines for edges 12 | for edge in [e for e in ph.ripsComplex if len(e) == 2]: 13 | # print(edge) 14 | pt1, pt2 = [data[pt] for pt in [n for n in edge]] 15 | # plt.gca().add_line(plt.Line2D(pt1,pt2)) 16 | line = plt.Polygon([pt1, pt2], closed=None, fill=None, edgecolor='r') 17 | plt.gca().add_line(line) 18 | 19 | # add triangles 20 | for triangle in [t for t in ph.ripsComplex if len(t) == 3]: 21 | pt1, pt2, pt3 = [data[pt] for pt in [n for n in triangle]] 22 | line = plt.Polygon([pt1, pt2, pt3], closed=False, 23 | color="blue", alpha=0.3, fill=True, edgecolor=None) 24 | plt.gca().add_line(line) 25 | plt.show() 26 | 27 | 28 | def graph_barcode(data, ph, homology_group=0): 29 | persistence = ph.transform(data) 30 | # this function just produces the barcode graph for each homology group 31 | xstart = [s[1][0] for s in persistence if s[0] == homology_group] 32 | xstop = [s[1][1] for s in persistence if s[0] == homology_group] 33 | y = [0.1 * x + 0.1 for x in range(len(xstart))] 34 | plt.hlines(y, xstart, xstop, color='b', lw=4) 35 | # Setup the plot 36 | ax = plt.gca() 37 | plt.ylim(0, max(y) + 0.1) 38 | ax.yaxis.set_major_formatter(plt.NullFormatter()) 39 | plt.xlabel('epsilon') 40 | plt.ylabel("Betti dim %s" % (homology_group,)) 41 | plt.show() 42 | -------------------------------------------------------------------------------- /tda/betti.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .matreduce import reduce_matrix 3 | 4 | 5 | # return the n-simplices in a complex 6 | def nSimplices(n, complex): 7 | nchain = [] 8 | for simplex in complex: 9 | if len(simplex) == (n + 1): 10 | nchain.append(simplex) 11 | if (nchain == []): 12 | nchain = [0] 13 | return nchain 14 | 15 | # check if simplex is a face of another simplex 16 | 17 | 18 | def checkFace(face, simplex): 19 | if simplex == 0: 20 | return 1 21 | elif set(face) < set(simplex): # if face is a subset of simplex 22 | return 1 23 | else: 24 | return 0 25 | 26 | # build boundary matrix for dimension n ---> (n-1) = p 27 | 28 | 29 | def boundaryMatrix(nchain, pchain): 30 | bmatrix = np.zeros((len(nchain), len(pchain))) 31 | i = 0 32 | for nSimplex in nchain: 33 | j = 0 34 | for pSimplex in pchain: 35 | bmatrix[i, j] = checkFace(pSimplex, nSimplex) 36 | j += 1 37 | i += 1 38 | return bmatrix.T 39 | 40 | 41 | def betti(complex): 42 | # get the maximum dimension of the simplicial complex, 2 in our example 43 | max_dim = len(max(complex, key=len)) 44 | # setup array to store n-th dimensional Betti numbers 45 | betti_array = np.zeros(max_dim) 46 | z_n = np.zeros(max_dim) # number of cycles (from cycle group) 47 | b_n = np.zeros(max_dim) # b_(n-1) boundary group 48 | # loop through each dimension starting from maximum to generate boundary 49 | # maps 50 | for i in range(max_dim): 51 | bm = 0 # setup n-th boundary matrix 52 | chain2 = nSimplices(i, complex) # n-th chain group 53 | if i == 0: # there is no n+1 boundary matrix in this case 54 | bm = 0 55 | z_n[i] = len(chain2) 56 | b_n[i] = 0 57 | else: 58 | chain1 = nSimplices(i - 1, complex) # (n-1)th chain group 59 | bm = reduce_matrix(boundaryMatrix(chain2, chain1)) 60 | z_n[i] = bm[2] 61 | b_n[i] = bm[1] # b_(n-1) 62 | 63 | for i in range(max_dim): # Calculate betti number: Z_n - B_n 64 | if (i + 1) < max_dim: 65 | betti_array[i] = z_n[i] - b_n[i + 1] 66 | else: 67 | # if there are no higher simplices, the boundary group of this 68 | # chain is 0 69 | betti_array[i] = z_n[i] - 0 70 | 71 | return betti_array 72 | -------------------------------------------------------------------------------- /tda/snf.pyx: -------------------------------------------------------------------------------- 1 | # Author: Carlos Xavier Hernandez 2 | 3 | import numpy as np 4 | cimport numpy as np 5 | 6 | from libc.stdlib cimport malloc, free 7 | 8 | cimport cython 9 | 10 | DTYPE = np.uint8 11 | 12 | ctypedef np.uint8_t DTYPE_t 13 | 14 | 15 | @cython.boundscheck(False) 16 | @cython.wraparound(False) 17 | def low(int i, np.ndarray[DTYPE_t, ndim=2] matrix): 18 | cdef np.ndarray[DTYPE_t, ndim=1] col 19 | cdef int j 20 | 21 | col = matrix[:, i] 22 | j = col.shape[0] - 1 23 | while j > -1: 24 | if col[j] == 1: 25 | return j 26 | j -= 1 27 | return col.shape[0] - 1 28 | 29 | 30 | @cython.boundscheck(False) 31 | @cython.wraparound(False) 32 | def isReduced(np.ndarray[DTYPE_t, ndim=2] matrix, int bound): 33 | cdef int i, j, low_j, low_i 34 | cdef int *low_js 35 | low_js = malloc((bound+1)*cython.sizeof(int)) 36 | 37 | for j in range(bound + 1): 38 | low_js[j] = low(j, matrix) 39 | 40 | for j in range(matrix.shape[1]): 41 | low_j = low_js[j] 42 | for i in range(j): # iterate through columns before column j 43 | low_i = low_js[i] 44 | if (low_j == low_i and low_j != bound): 45 | return i, j # return column i to add to column j 46 | return 0, 0 47 | 48 | 49 | @cython.boundscheck(False) 50 | @cython.wraparound(False) 51 | def bitwise_xor(np.ndarray[DTYPE_t, ndim=1] col_i, 52 | np.ndarray[DTYPE_t, ndim=1] col_j, 53 | np.ndarray[DTYPE_t, ndim=1] out): 54 | cdef int i, n 55 | 56 | n = out.shape[0] - 1 57 | while n > -1: 58 | if (col_i[n] + col_j[n]) == 1: 59 | out[n] = 1 60 | n -= 1 61 | 62 | return out 63 | 64 | 65 | @cython.boundscheck(False) 66 | @cython.wraparound(False) 67 | def smith_normal_form(np.ndarray[DTYPE_t, ndim=2] matrix): 68 | cdef int i, j, n, m 69 | cdef np.ndarray[DTYPE_t, ndim=1] col_i, col_j, out 70 | cdef np.ndarray[DTYPE_t, ndim=2] reduced_matrix 71 | 72 | reduced_matrix = matrix.copy() 73 | m = reduced_matrix.shape[0] 74 | 75 | i, j = isReduced(reduced_matrix, m - 1) 76 | while (i != 0 and j != 0): 77 | col_j = reduced_matrix[:, j] 78 | col_i = reduced_matrix[:, i] 79 | 80 | out = np.zeros(m, dtype=DTYPE) 81 | reduced_matrix[:, j] = bitwise_xor(col_i, col_j, out) 82 | i, j = isReduced(reduced_matrix, m - 1) 83 | return reduced_matrix 84 | -------------------------------------------------------------------------------- /tda/SimplicialComplex.py: -------------------------------------------------------------------------------- 1 | #This set of functions allows for building a Vietoris-Rips simplicial complex from point data 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | def euclidianDist(a,b): 7 | return np.linalg.norm(a - b) #euclidian distance metric 8 | 9 | #Build neighorbood graph 10 | def buildGraph(raw_data, epsilon = 3.1, metric=euclidianDist): #raw_data is a numpy array 11 | nodes = [x for x in range(raw_data.shape[0])] #initialize node set, reference indices from original data array 12 | edges = [] #initialize empty edge array 13 | weights = [] #initialize weight array, stores the weight (which in this case is the distance) for each edge 14 | for i in range(raw_data.shape[0]): #iterate through each data point 15 | for j in range(raw_data.shape[0]-i): #inner loop to calculate pairwise point distances 16 | a = raw_data[i] 17 | b = raw_data[j+i] #each simplex is a set (no order), hence [0,1] = [1,0]; so only store one 18 | if (i != j+i): 19 | dist = metric(a,b) 20 | if dist <= epsilon: 21 | edges.append({i,j+i}) #add edge 22 | weights.append([len(edges)-1,dist]) #store index and weight 23 | return nodes,edges,weights 24 | 25 | def lower_nbrs(nodeSet, edgeSet, node): 26 | return {x for x in nodeSet if {x,node} in edgeSet and node > x} 27 | 28 | def rips(graph, k): 29 | nodes, edges = graph[0:2] 30 | VRcomplex = [{n} for n in nodes] 31 | for e in edges: #add 1-simplices (edges) 32 | VRcomplex.append(e) 33 | for i in range(k): 34 | for simplex in [x for x in VRcomplex if len(x)==i+2]: #skip 0-simplices 35 | #for each u in simplex 36 | nbrs = set.intersection(*[lower_nbrs(nodes, edges, z) for z in simplex]) 37 | for nbr in nbrs: 38 | VRcomplex.append(set.union(simplex,{nbr})) 39 | return VRcomplex 40 | 41 | def drawComplex(origData, ripsComplex, axes=[-6,8,-6,6]): 42 | plt.clf() 43 | plt.axis(axes) 44 | plt.scatter(origData[:,0],origData[:,1]) #plotting just for clarity 45 | for i, txt in enumerate(origData): 46 | plt.annotate(i, (origData[i][0]+0.05, origData[i][1])) #add labels 47 | 48 | #add lines for edges 49 | for edge in [e for e in ripsComplex if len(e)==2]: 50 | #print(edge) 51 | pt1,pt2 = [origData[pt] for pt in [n for n in edge]] 52 | #plt.gca().add_line(plt.Line2D(pt1,pt2)) 53 | line = plt.Polygon([pt1,pt2], closed=None, fill=None, edgecolor='r') 54 | plt.gca().add_line(line) 55 | 56 | #add triangles 57 | for triangle in [t for t in ripsComplex if len(t)==3]: 58 | pt1,pt2,pt3 = [origData[pt] for pt in [n for n in triangle]] 59 | line = plt.Polygon([pt1,pt2,pt3], closed=False, color="blue",alpha=0.3, fill=True, edgecolor=None) 60 | plt.gca().add_line(line) 61 | plt.show() 62 | -------------------------------------------------------------------------------- /tda/FilteredSimplicialComplex.py: -------------------------------------------------------------------------------- 1 | #This set of functions allows for building a Vietoris-Rips simplicial complex from point data 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | def euclidianDist(a,b): 7 | return np.linalg.norm(a - b) #euclidian distance metric 8 | 9 | #Build neighorbood graph 10 | def buildGraph(raw_data, epsilon = 3.1, metric=euclidianDist): #raw_data is a numpy array 11 | nodes = [x for x in range(raw_data.shape[0])] #initialize node set, reference indices from original data array 12 | edges = [] #initialize empty edge array 13 | weights = [] #initialize weight array, stores the weight (which in this case is the distance) for each edge 14 | for i in range(raw_data.shape[0]): #iterate through each data point 15 | for j in range(raw_data.shape[0]-i): #inner loop to calculate pairwise point distances 16 | a = raw_data[i] 17 | b = raw_data[j+i] #each simplex is a set (no order), hence [0,1] = [1,0]; so only store one 18 | if (i != j+i): 19 | dist = metric(a,b) 20 | if dist <= epsilon: 21 | edges.append({i,j+i}) #add edge if distance between points is < epsilon 22 | weights.append([len(edges)-1,dist]) #store index and weight 23 | return nodes,edges,weights 24 | 25 | def lower_nbrs(nodeSet, edgeSet, node): #lowest neighbors based on arbitrary ordering of simplices 26 | return {x for x in nodeSet if {x,node} in edgeSet and node > x} 27 | 28 | def ripsFiltration(nodes, edges, weights, k): #k is the maximal dimension we want to compute 29 | VRcomplex = [{n} for n in nodes] 30 | filter_values = [0 for j in VRcomplex] #vertices have filter value of 0 31 | for i in range(len(edges)): #add 1-simplices (edges) and associated filter values 32 | VRcomplex.append(edges[i]) 33 | filter_values.append(weights[i]) 34 | for i in range(k): 35 | for simplex in [x for x in VRcomplex if len(x)==i+2]: #skip 0-simplices 36 | #for each u in simplex 37 | nbrs = set.intersection(*[lower_nbrs(nodes, edges, z) for z in simplex]) 38 | for nbr in nbrs: 39 | VRcomplex.append(set.union(simplex,{nbr})) 40 | return VRcomplex, filter_values 41 | 42 | def getFilterValue(simplex, complex, filter_values): 43 | return filter_values[complex.index(simplex)] 44 | 45 | def drawComplex(origData, ripsComplex, axes=[-6,8,-6,6]): 46 | plt.clf() 47 | plt.axis(axes) 48 | plt.scatter(origData[:,0],origData[:,1]) #plotting just for clarity 49 | for i, txt in enumerate(origData): 50 | plt.annotate(i, (origData[i][0]+0.05, origData[i][1])) #add labels 51 | 52 | #add lines for edges 53 | for edge in [e for e in ripsComplex if len(e)==2]: 54 | #print(edge) 55 | pt1,pt2 = [origData[pt] for pt in [n for n in edge]] 56 | #plt.gca().add_line(plt.Line2D(pt1,pt2)) 57 | line = plt.Polygon([pt1,pt2], closed=None, fill=None, edgecolor='r') 58 | plt.gca().add_line(line) 59 | 60 | #add triangles 61 | for triangle in [t for t in ripsComplex if len(t)==3]: 62 | pt1,pt2,pt3 = [origData[pt] for pt in [n for n in triangle]] 63 | line = plt.Polygon([pt1,pt2,pt3], closed=False, color="blue",alpha=0.3, fill=True, edgecolor=None) 64 | plt.gca().add_line(line) 65 | plt.show() 66 | -------------------------------------------------------------------------------- /tda/persistent_homology.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import functools 3 | 4 | import numpy as np 5 | import networkx as nx 6 | from scipy.spatial.distance import squareform, pdist 7 | 8 | from .snf import low, smith_normal_form 9 | 10 | __all__ = ['PersistentHomology'] 11 | 12 | 13 | def buildGraph(data, epsilon=1., metric='euclidean', p=2): 14 | D = squareform(pdist(data, metric=metric, p=p)) 15 | D[D >= epsilon] = 0. 16 | G = nx.Graph(D) 17 | edges = list(map(set, G.edges())) 18 | weights = [G.get_edge_data(u, v)['weight'] for u, v in G.edges()] 19 | return G.nodes(), edges, weights 20 | 21 | 22 | def lower_nbrs(nodeSet, edgeSet, node): 23 | return {x for x in nodeSet if {x, node} in edgeSet and node > x} 24 | 25 | 26 | def rips(nodes, edges, k): 27 | VRcomplex = [{n} for n in nodes] 28 | for e in edges: # add 1-simplices (edges) 29 | VRcomplex.append(e) 30 | for i in range(k): 31 | # skip 0-simplices 32 | for simplex in [x for x in VRcomplex if len(x) == i + 2]: 33 | # for each u in simplex 34 | nbrs = set.intersection( 35 | *[lower_nbrs(nodes, edges, z) for z in simplex]) 36 | for nbr in nbrs: 37 | VRcomplex.append(set.union(simplex, {nbr})) 38 | return VRcomplex 39 | 40 | 41 | def ripsFiltration(graph, k): 42 | nodes, edges, weights = graph 43 | VRcomplex = [{n} for n in nodes] 44 | filter_values = [0 for j in VRcomplex] # vertices have filter value of 0 45 | # add 1-simplices (edges) and associated filter values 46 | for i in range(len(edges)): 47 | VRcomplex.append(edges[i]) 48 | filter_values.append(weights[i]) 49 | if k > 1: 50 | for i in range(k): 51 | # skip 0-simplices and 1-simplices 52 | for simplex in [x for x in VRcomplex if len(x) == i + 2]: 53 | # for each u in simplex 54 | nbrs = set.intersection( 55 | *[lower_nbrs(nodes, edges, z) for z in simplex]) 56 | for nbr in nbrs: 57 | newSimplex = set.union(simplex, {nbr}) 58 | VRcomplex.append(newSimplex) 59 | filter_values.append(getFilterValue( 60 | newSimplex, VRcomplex, filter_values)) 61 | 62 | # sort simplices according to filter values 63 | return sortComplex(VRcomplex, filter_values) 64 | 65 | 66 | def getFilterValue(simplex, edges, weights): 67 | oneSimplices = list(itertools.combinations(simplex, 2)) 68 | max_weight = 0 69 | for oneSimplex in oneSimplices: 70 | filter_value = weights[edges.index(set(oneSimplex))] 71 | if filter_value > max_weight: 72 | max_weight = filter_value 73 | return max_weight 74 | 75 | 76 | def compare(item1, item2): 77 | if len(item1[0]) == len(item2[0]): 78 | if item1[1] == item2[1]: 79 | if sum(item1[0]) > sum(item2[0]): 80 | return 1 81 | else: 82 | return -1 83 | else: 84 | if item1[1] > item2[1]: 85 | return 1 86 | else: 87 | return -1 88 | else: 89 | if len(item1[0]) > len(item2[0]): 90 | return 1 91 | else: 92 | return -1 93 | 94 | 95 | def sortComplex(filterComplex, filterValues): 96 | pairedList = zip(filterComplex, filterValues) 97 | sortedComplex = sorted(pairedList, key=functools.cmp_to_key(compare)) 98 | sortedComplex = [list(t) for t in zip(*sortedComplex)] 99 | 100 | return sortedComplex 101 | 102 | 103 | def nSimplices(n, filterComplex): 104 | nchain = [] 105 | nfilters = [] 106 | for i in range(len(filterComplex[0])): 107 | simplex = filterComplex[0][i] 108 | if len(simplex) == (n + 1): 109 | nchain.append(simplex) 110 | nfilters.append(filterComplex[1][i]) 111 | if (nchain == []): 112 | nchain = [0] 113 | return nchain, nfilters 114 | 115 | 116 | def checkFace(face, simplex): 117 | if simplex == 0: 118 | return 1 119 | 120 | elif (set(face) < set(simplex) and (len(face) == (len(simplex) - 1))): 121 | return 1 122 | else: 123 | return 0 124 | 125 | 126 | def filterBoundaryMatrix(filterComplex): 127 | bmatrix = np.zeros( 128 | (len(filterComplex[0]), len(filterComplex[0])), dtype=np.uint8) 129 | 130 | i = 0 131 | for colSimplex in filterComplex[0]: 132 | j = 0 133 | for rowSimplex in filterComplex[0]: 134 | bmatrix[j, i] = checkFace(rowSimplex, colSimplex) 135 | j += 1 136 | i += 1 137 | return bmatrix 138 | 139 | 140 | def readIntervals(reduced_matrix, filterValues): 141 | intervals = [] 142 | m = reduced_matrix.shape[1] 143 | for j in range(m): 144 | low_j = low(j, reduced_matrix) 145 | if low_j == (m - 1): 146 | interval_start = [j, -1] 147 | intervals.append(interval_start) 148 | 149 | else: 150 | feature = intervals.index([low_j, -1]) 151 | intervals[feature][1] = j 152 | epsilon_start = filterValues[intervals[feature][0]] 153 | epsilon_end = filterValues[j] 154 | if epsilon_start == epsilon_end: 155 | intervals.remove(intervals[feature]) 156 | 157 | return intervals 158 | 159 | 160 | def readPersistence(intervals, filterComplex): 161 | persistence = [] 162 | for interval in intervals: 163 | start = interval[0] 164 | end = interval[1] 165 | 166 | homology_group = (len(filterComplex[0][start]) - 1) 167 | epsilon_start = filterComplex[1][start] 168 | epsilon_end = filterComplex[1][end] 169 | persistence.append([homology_group, [epsilon_start, epsilon_end]]) 170 | 171 | return persistence 172 | 173 | 174 | class PersistentHomology(object): 175 | 176 | def __init__(self, epsilon=1., k=3): 177 | self.epsilon = epsilon 178 | self.k = k 179 | 180 | def fit(self, X): 181 | self.graph = buildGraph(X, epsilon=self.epsilon) 182 | self.ripsComplex = ripsFiltration(self.graph, k=self.k) 183 | self.boundary_matrix = filterBoundaryMatrix(self.ripsComplex) 184 | self.reduced_boundary_matrix = smith_normal_form(self.boundary_matrix) 185 | return self 186 | 187 | def transform(self, X): 188 | intervals = readIntervals(self.reduced_boundary_matrix, 189 | self.ripsComplex[1]) 190 | persistence = readPersistence(intervals, self.ripsComplex) 191 | return persistence 192 | 193 | def fit_transform(self, X): 194 | self.fit(X) 195 | return self.transform(X) 196 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------