phd-ms 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phd-ms/__init__.py +8 -0
- phd-ms/_utils/__init__.py +3 -0
- phd-ms/_utils/_benchmark.py +28 -0
- phd-ms/_utils/_filtration.py +141 -0
- phd-ms/_utils/_leiden.py +41 -0
- phd-ms/tools/__init__.py +8 -0
- phd-ms/tools/_phdms.py +293 -0
- phd_ms-0.0.0.dist-info/METADATA +70 -0
- phd_ms-0.0.0.dist-info/RECORD +12 -0
- phd_ms-0.0.0.dist-info/WHEEL +5 -0
- phd_ms-0.0.0.dist-info/licenses/LICENSE.md +21 -0
- phd_ms-0.0.0.dist-info/top_level.txt +1 -0
phd-ms/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import ot
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
from scipy.special import logit
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
def clusters_to_distribution(clusters):
|
|
8
|
+
if isinstance(clusters,pd.Series):
|
|
9
|
+
distribution = np.zeros((len(clusters.cat.categories.tolist()),len(clusters)))
|
|
10
|
+
for i in range(len(clusters)):
|
|
11
|
+
if isinstance(clusters.iloc[i],float):
|
|
12
|
+
continue
|
|
13
|
+
index = next(j for j in range(len(clusters.cat.categories.tolist())) if clusters.cat.categories.tolist()[j] == clusters.iloc[i])
|
|
14
|
+
distribution[index,i] = 1
|
|
15
|
+
cmat = distribution.transpose().copy()
|
|
16
|
+
|
|
17
|
+
elif isinstance(clusters,np.ndarray):
|
|
18
|
+
cmat = clusters.copy()
|
|
19
|
+
distribution = np.zeros((clusters.shape[1],clusters.shape[0]))
|
|
20
|
+
for i in range(clusters.shape[0]): # Iterate over rows
|
|
21
|
+
for j in range(clusters.shape[1]): # Iterate over columns
|
|
22
|
+
if clusters[i,j] < 0.01:
|
|
23
|
+
distribution[j,i] = 0
|
|
24
|
+
else:
|
|
25
|
+
distribution[j,i] = 1
|
|
26
|
+
|
|
27
|
+
distribution /= np.sum(distribution,axis=1,keepdims=True)
|
|
28
|
+
return distribution,cmat
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import gudhi as gd
|
|
3
|
+
|
|
4
|
+
def filt_to_matrix(simp_tree,threshold=.05):
|
|
5
|
+
matrix = np.ones((simp_tree.num_vertices(),simp_tree.num_vertices()))
|
|
6
|
+
#print(simp_tree.num_vertices())
|
|
7
|
+
max_node = np.max(list(x[1] for x in simp_tree.get_filtration() if len(x[0])==1))
|
|
8
|
+
for simplex in simp_tree.get_filtration():
|
|
9
|
+
|
|
10
|
+
if len(simplex[0]) == 2:
|
|
11
|
+
|
|
12
|
+
#if simp_tree.filtration([simplex[0][0]]) < threshold and simp_tree.filtration([simplex[0][1]]) < threshold:
|
|
13
|
+
matrix[simplex[0][0],simplex[0][1]] = simplex[1]
|
|
14
|
+
|
|
15
|
+
if matrix[simplex[0][0],simplex[0][1]] == 0:
|
|
16
|
+
matrix[simplex[0][0],simplex[0][1]] = matrix[simplex[0][0],simplex[0][1]]+.0001
|
|
17
|
+
|
|
18
|
+
matrix[simplex[0][1],simplex[0][0]] = matrix[simplex[0][0],simplex[0][1]]
|
|
19
|
+
|
|
20
|
+
return matrix
|
|
21
|
+
|
|
22
|
+
def union_find_dmat(dmat, edge_cut):
|
|
23
|
+
|
|
24
|
+
N = dmat.shape[0]
|
|
25
|
+
|
|
26
|
+
class Node:
|
|
27
|
+
def __init__ (self, loc, birth = 0):
|
|
28
|
+
self.loc = loc
|
|
29
|
+
self.parent = self.loc
|
|
30
|
+
self.birth = birth
|
|
31
|
+
def __str__(self):
|
|
32
|
+
return self.label
|
|
33
|
+
def __int__(self):
|
|
34
|
+
return self.loc
|
|
35
|
+
|
|
36
|
+
def Union(x, y, L):
|
|
37
|
+
xRoot = Find(x,L)
|
|
38
|
+
yRoot = Find(y,L)
|
|
39
|
+
if xRoot.loc != yRoot.loc:
|
|
40
|
+
if xRoot.birth > yRoot.birth:
|
|
41
|
+
xRoot.parent = yRoot.loc
|
|
42
|
+
return xRoot.loc # This should return the one that got killed
|
|
43
|
+
else:
|
|
44
|
+
yRoot.parent = xRoot.loc
|
|
45
|
+
return yRoot.loc # This should return the one that got killed
|
|
46
|
+
|
|
47
|
+
def Find(x,L):
|
|
48
|
+
if x.parent == x.loc:
|
|
49
|
+
return L[x.parent]
|
|
50
|
+
else:
|
|
51
|
+
return Find(L[x.parent], L)
|
|
52
|
+
|
|
53
|
+
# A list of allowed neighbors for each node
|
|
54
|
+
nb_dic = {}
|
|
55
|
+
for i in range(N):
|
|
56
|
+
tmp_list = []
|
|
57
|
+
for j in range(N):
|
|
58
|
+
if dmat[i][j] <= edge_cut and i!=j:
|
|
59
|
+
tmp_list.append(j)
|
|
60
|
+
nb_dic[i] = tmp_list
|
|
61
|
+
|
|
62
|
+
simplex_collection = []
|
|
63
|
+
simplex_index = {}
|
|
64
|
+
for i in range(N):
|
|
65
|
+
simplex_collection.append( ( [i], 0.0 ) )
|
|
66
|
+
for i in range(N-1):
|
|
67
|
+
for j in range(i+1, N):
|
|
68
|
+
if j in nb_dic[i]:
|
|
69
|
+
simplex_collection.append( ([i,j], dmat[i][j]))
|
|
70
|
+
|
|
71
|
+
filtration = []
|
|
72
|
+
tmp_list = []
|
|
73
|
+
for s in simplex_collection:
|
|
74
|
+
tmp_list.append(s[1])
|
|
75
|
+
simplex_order = np.argsort(tmp_list)
|
|
76
|
+
simplex_order[:N] = np.arange(N)[:]
|
|
77
|
+
cnt = 0; complex_filtration_detail = [];
|
|
78
|
+
for i in simplex_order:
|
|
79
|
+
if len(simplex_collection[i][0]) == 1:
|
|
80
|
+
complex_filtration_detail.append( [ (0,[]), simplex_collection[i][1], set(simplex_collection[i][0]) ] )
|
|
81
|
+
simplex_index[(i)] = cnt
|
|
82
|
+
cnt += 1
|
|
83
|
+
elif len(simplex_collection[i][0]) == 2:
|
|
84
|
+
n1 = simplex_collection[i][0][0];
|
|
85
|
+
n2 = simplex_collection[i][0][1];
|
|
86
|
+
complex_filtration_detail.append( [ (1, [n1,n2]), simplex_collection[i][1], set(simplex_collection[i][0]) ] )
|
|
87
|
+
simplex_index[(n1,n2)] = cnt
|
|
88
|
+
cnt += 1
|
|
89
|
+
persDgm_pairs = []
|
|
90
|
+
L = {0: Node(0, birth = 0)}
|
|
91
|
+
Cocycles = [[i] for i in range(N)]
|
|
92
|
+
Cocycle_fvalues = [[0.0] for i in range(N)]
|
|
93
|
+
for i in range(1, len(complex_filtration_detail)):
|
|
94
|
+
f = complex_filtration_detail[i]
|
|
95
|
+
if f[0][0] == 0:
|
|
96
|
+
L[i] = Node(i, birth=i)
|
|
97
|
+
else:
|
|
98
|
+
[n1,n2] = f[0][1]
|
|
99
|
+
killed = Union(L[n1], L[n2], L)
|
|
100
|
+
if n1 == killed:
|
|
101
|
+
lived = n2;
|
|
102
|
+
else:
|
|
103
|
+
lived = n1
|
|
104
|
+
if killed != None:
|
|
105
|
+
pair = (L[killed].birth, i)
|
|
106
|
+
persDgm_pairs.append(pair)
|
|
107
|
+
# print killed, n1, n2, dmat[n1,n2], L[killed].parent
|
|
108
|
+
Cocycles[L[killed].parent].extend(Cocycles[killed])
|
|
109
|
+
Cocycle_fvalues[L[killed].parent].extend([dmat[n1,n2] for i in range(len(Cocycles[killed]))])
|
|
110
|
+
|
|
111
|
+
setReps = [Find(L[v],L).loc for v in L.keys()]
|
|
112
|
+
persDgm = []
|
|
113
|
+
pair_births = []
|
|
114
|
+
for d in persDgm_pairs:
|
|
115
|
+
persDgm.append([complex_filtration_detail[d[0]][1], complex_filtration_detail[d[1]][1]])
|
|
116
|
+
pair_births.append(d[0])
|
|
117
|
+
unpaired = list(set(setReps))
|
|
118
|
+
for i in unpaired:
|
|
119
|
+
persDgm.append([complex_filtration_detail[i][1], np.inf])
|
|
120
|
+
pair_births.append(i)
|
|
121
|
+
pair_births = np.asarray(pair_births)
|
|
122
|
+
pair_births_index = np.argsort(pair_births)
|
|
123
|
+
diagram_0d = []
|
|
124
|
+
for i in pair_births_index:
|
|
125
|
+
d = persDgm[i]
|
|
126
|
+
diagram_0d.append([0,d[0],d[1]])
|
|
127
|
+
# print diagram_0d
|
|
128
|
+
|
|
129
|
+
# print Cocycles
|
|
130
|
+
# print Cocycle_fvalues
|
|
131
|
+
return [diagram_0d, Cocycles, Cocycle_fvalues]
|
|
132
|
+
|
|
133
|
+
def get_sub_features(cocycles,diagram_0d,feature,feature_list):
|
|
134
|
+
|
|
135
|
+
t,j = next(((set(cocycles[j]),j) for j in range(len(cocycles)) if set(cocycles[j])<feature),([],[]))
|
|
136
|
+
if t != []:
|
|
137
|
+
feature_list.append((feature-t,diagram_0d[j][2]))
|
|
138
|
+
#feature_list.append((t,diagram_0d[j][2]))
|
|
139
|
+
#cocycles.remove(cocycles[j])
|
|
140
|
+
feature_list = get_sub_features(cocycles,diagram_0d,feature-t,feature_list)
|
|
141
|
+
return feature_list
|
phd-ms/_utils/_leiden.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scanpy as sc
|
|
3
|
+
from sklearn.metrics import adjusted_rand_score,adjusted_mutual_info_score
|
|
4
|
+
|
|
5
|
+
def leiden(adata,res=np.linspace(start=0.2,stop=1.2,num=6),show=True,scores=False,embedding='X_gst',res_keys=[],ground_truth='cluster'):
|
|
6
|
+
if not res_keys:
|
|
7
|
+
res_keys = ['leiden_'+str(r) for r in res]
|
|
8
|
+
sc.pp.neighbors(adata,use_rep=embedding,n_neighbors=5)
|
|
9
|
+
|
|
10
|
+
sc.tl.umap(adata)
|
|
11
|
+
for i in range(len(res)):
|
|
12
|
+
sc.tl.leiden(
|
|
13
|
+
adata,
|
|
14
|
+
key_added=res_keys[i],
|
|
15
|
+
resolution=res[i],
|
|
16
|
+
n_iterations=2,
|
|
17
|
+
directed=False,
|
|
18
|
+
)
|
|
19
|
+
if show:
|
|
20
|
+
sc.pl.umap(adata, color=res_keys[i])
|
|
21
|
+
sc.pl.embedding(adata, basis="spatial", color=res_keys[i])
|
|
22
|
+
if scores:
|
|
23
|
+
print('Resolution, adjusted mutual info, adjusted rand:')
|
|
24
|
+
print(cluster_metrics(adata.obs[ground_truth],adata.obs[res_keys[i]]))
|
|
25
|
+
|
|
26
|
+
return adata
|
|
27
|
+
|
|
28
|
+
def cluster_metrics(cluster,r,truth=None):
|
|
29
|
+
clusters = set(int(j) for j in cluster.cat.categories.tolist())
|
|
30
|
+
if not(truth is None):
|
|
31
|
+
l1 = np.zeros(len(truth))
|
|
32
|
+
l2 = np.zeros(len(truth))
|
|
33
|
+
for i in range(0,len(truth.cat.categories.tolist())):
|
|
34
|
+
guy = list(n for n in range(0,len(truth)) if truth.iloc[n]==truth.cat.categories.tolist()[i])
|
|
35
|
+
l1[guy] = i
|
|
36
|
+
for i in range(0,len(cluster.cat.categories.tolist())):
|
|
37
|
+
guy = list(n for n in range(0,len(cluster)) if cluster.iloc[n]==cluster.cat.categories.tolist()[i])
|
|
38
|
+
l2[guy] = i
|
|
39
|
+
return r,len(clusters),adjusted_mutual_info_score(l1,l2),adjusted_rand_score(l1,l2)
|
|
40
|
+
else:
|
|
41
|
+
return r,len(clusters)
|
phd-ms/tools/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
from ._phdms import map_multiscale
|
|
3
|
+
from ._phdms import preprocess_leiden
|
|
4
|
+
from ._phdms import cluster_filtration
|
|
5
|
+
from ._phdms import ground_truth_benchmark
|
|
6
|
+
from ._phdms import plot_multiscale
|
|
7
|
+
from ._phdms import construct_clustering
|
|
8
|
+
from ._phdms import point_click_multiscale
|
phd-ms/tools/_phdms.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import scanpy as sc
|
|
5
|
+
import gudhi as gd
|
|
6
|
+
from .._utils import leiden, filt_to_matrix, union_find_dmat, get_sub_features, clusters_to_distribution
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
from scipy.special import logit
|
|
9
|
+
import ot
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
def preprocess_leiden(input_file,output_file='',emb='X_gst',resolution=np.linspace(start=0.05,stop=.95,num=10),res_keys=[],ground_truth='cluster'):
|
|
13
|
+
|
|
14
|
+
adata = sc.read_h5ad(input_file+'.h5ad')
|
|
15
|
+
leiden(adata,res=resolution,show=False,embedding=emb,res_keys=res_keys,ground_truth=ground_truth)
|
|
16
|
+
adata.write_h5ad(output_file+'.h5ad')
|
|
17
|
+
|
|
18
|
+
def cluster_filtration(adata,res_keys,index='containment',order=[]):
|
|
19
|
+
|
|
20
|
+
num_cells = adata.obsm['spatial'].shape[0]
|
|
21
|
+
|
|
22
|
+
#Reorder resolutions if order is given
|
|
23
|
+
if order:
|
|
24
|
+
res_keys = [res_keys[i] for i in reversed(order)]
|
|
25
|
+
|
|
26
|
+
#Create the filtration
|
|
27
|
+
leiden_complex = gd.SimplexTree()
|
|
28
|
+
|
|
29
|
+
#List of number of clusters at each resolution
|
|
30
|
+
num_clusters = []
|
|
31
|
+
#List of sets of indices for each cluster at each resolution
|
|
32
|
+
clusters = []
|
|
33
|
+
|
|
34
|
+
#We want to iterate through pairs of neighboring resolutions
|
|
35
|
+
for i in range(0,len(res_keys)-1):
|
|
36
|
+
|
|
37
|
+
#Initialize the pair of resolutions we look at
|
|
38
|
+
fine = adata.obs[res_keys[i]]
|
|
39
|
+
coarse = adata.obs[res_keys[i+1]]
|
|
40
|
+
fine_clusters = [int(j) for j in fine.cat.categories.tolist()]
|
|
41
|
+
coarse_clusters = [int(j) for j in coarse.cat.categories.tolist()]
|
|
42
|
+
|
|
43
|
+
#Update number of clusters and clusters
|
|
44
|
+
#We want to add this information only on the first iteration
|
|
45
|
+
if i == 0:
|
|
46
|
+
num_clusters.append(len(fine_clusters))
|
|
47
|
+
for j in fine_clusters:
|
|
48
|
+
fine_j = set(n for n in range(0,len(fine)) if int(fine.iloc[n])==j)
|
|
49
|
+
clusters.append(fine_j)
|
|
50
|
+
|
|
51
|
+
for k in coarse_clusters:
|
|
52
|
+
coarse_k = set(n for n in range(0,len(coarse)) if int(coarse.iloc[n])==k)
|
|
53
|
+
clusters.append(coarse_k)
|
|
54
|
+
num_clusters.append(len(coarse_clusters))
|
|
55
|
+
|
|
56
|
+
#Now, we compare every cluster in the fine resolution with every cluster in the coarse resolution
|
|
57
|
+
for j in fine_clusters:
|
|
58
|
+
|
|
59
|
+
#The j-th cluster in the fine resolution
|
|
60
|
+
fine_j = set(n for n in range(0,len(fine)) if int(fine.iloc[n])==j)
|
|
61
|
+
|
|
62
|
+
for k in coarse_clusters:
|
|
63
|
+
#The k-th cluster in the coarse resolution
|
|
64
|
+
coarse_k = set(n for n in range(0,len(coarse)) if int(coarse.iloc[n])==k)
|
|
65
|
+
|
|
66
|
+
#Compute union and intersection of the fine and coarse clusters
|
|
67
|
+
intersection = fine_j.intersection(coarse_k)
|
|
68
|
+
union = fine_j.union(coarse_k)
|
|
69
|
+
|
|
70
|
+
#If they intersect, we compute a filtration value
|
|
71
|
+
if intersection:
|
|
72
|
+
#Compute filtration value as either containment or Jaccard index
|
|
73
|
+
if index == 'containment':
|
|
74
|
+
filt = 1 - len(intersection)/len(fine_j)
|
|
75
|
+
elif index == 'jaccard':
|
|
76
|
+
filt = 1 - len(intersection)/len(union)
|
|
77
|
+
|
|
78
|
+
#Add edge between clusters with this filtration value
|
|
79
|
+
leiden_complex.insert([sum(num_clusters[:-2])+j,sum(num_clusters[0:-1])+k],filt)
|
|
80
|
+
|
|
81
|
+
#Make sure that all the clusters have filtration value 0
|
|
82
|
+
for i in range(0,sum(num_clusters)):
|
|
83
|
+
leiden_complex.assign_filtration([i],0)
|
|
84
|
+
return leiden_complex,clusters
|
|
85
|
+
|
|
86
|
+
def map_multiscale(spatial,cluster_complex,clusterings,num_domains=0,filt=0,plots="on",order='persistence',redundant_filter=False):
|
|
87
|
+
|
|
88
|
+
dmat = filt_to_matrix(cluster_complex)
|
|
89
|
+
diagram_0d,cocycles,_= union_find_dmat(dmat,edge_cut=1)
|
|
90
|
+
if num_domains == 0:
|
|
91
|
+
num_domains = len(cocycles)
|
|
92
|
+
|
|
93
|
+
#Sort persistent homology results by death time
|
|
94
|
+
diagram_0d,cocycles = zip(*sorted(zip(diagram_0d,cocycles),key=lambda x: x[0][2],reverse=True))
|
|
95
|
+
#Filter out non-persistent results if desired.
|
|
96
|
+
if filt > 0:
|
|
97
|
+
index = next(i for i in range(len(diagram_0d)) if diagram_0d[i][2]<filt)
|
|
98
|
+
diagram_0d = diagram_0d[:index]
|
|
99
|
+
cocycles = list(cocycles[:index])
|
|
100
|
+
domains = []
|
|
101
|
+
|
|
102
|
+
#Iterate through persistent components
|
|
103
|
+
for n in range(len(cocycles)+1):
|
|
104
|
+
if n == 0:
|
|
105
|
+
feature_list = [(set(cocycles[n]),1)]
|
|
106
|
+
elif n == len(cocycles):
|
|
107
|
+
break
|
|
108
|
+
else:
|
|
109
|
+
feature_list = [(set(cocycles[n]),diagram_0d[n][2])]
|
|
110
|
+
#find all the clusters that belong to the multiscale domain
|
|
111
|
+
feature_list = get_sub_features(cocycles,diagram_0d,feature_list[0][0],feature_list)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
x = spatial[:,0]
|
|
115
|
+
y = spatial[:,1]
|
|
116
|
+
coreness = np.ones(len(x))
|
|
117
|
+
#Compute coreness score for each point in the tissue
|
|
118
|
+
#iterate backwards through features to find filtration value where point first appears in multiscale domain
|
|
119
|
+
feature_list = sorted(feature_list,key= lambda x: x[1],reverse=True)
|
|
120
|
+
tracker = set()
|
|
121
|
+
for i in range(len(feature_list)-1, -1, -1):
|
|
122
|
+
|
|
123
|
+
spots = set()
|
|
124
|
+
for clust in feature_list[i][0]:
|
|
125
|
+
spots = set.union(spots,clusterings[clust])
|
|
126
|
+
|
|
127
|
+
coreness[list(spots-tracker)] = feature_list[i][1]
|
|
128
|
+
tracker = set.union(tracker,spots)
|
|
129
|
+
|
|
130
|
+
nontrivial = True
|
|
131
|
+
#Filter out results which share too similar proportions to previously examined domains
|
|
132
|
+
if redundant_filter:
|
|
133
|
+
for guy in domains:
|
|
134
|
+
zz = set(i for i in range(len(coreness)) if guy[i] < 1)
|
|
135
|
+
gg = set(i for i in range(len(coreness)) if coreness[i] < 1)
|
|
136
|
+
if len(gg.intersection(zz)) > redundant_filter*len(gg.union(zz)):
|
|
137
|
+
nontrivial = False
|
|
138
|
+
break
|
|
139
|
+
if nontrivial:
|
|
140
|
+
domains.append(coreness)
|
|
141
|
+
|
|
142
|
+
#Default order is by death time
|
|
143
|
+
#Ordered by size of domain if specified
|
|
144
|
+
if order == 'size':
|
|
145
|
+
domains = sorted(domains,key = lambda x : sum(x))
|
|
146
|
+
elif order == 'persistence':
|
|
147
|
+
domains = domains
|
|
148
|
+
|
|
149
|
+
if len(domains) > num_domains:
|
|
150
|
+
domains = domains[:num_domains]
|
|
151
|
+
|
|
152
|
+
#Normalize the coreness values to be between 0 and 1
|
|
153
|
+
for i in range(len(domains)):
|
|
154
|
+
max = np.max(list(z for z in domains[i]))
|
|
155
|
+
min = np.min(list(z for z in domains[i]))
|
|
156
|
+
z = list(1-(domains[i][j]-min)/(max-min) for j in range(len(domains[i])))
|
|
157
|
+
|
|
158
|
+
domains[i] = np.array(z)
|
|
159
|
+
|
|
160
|
+
if plots == 'on':
|
|
161
|
+
for d in domains:
|
|
162
|
+
plot_multiscale(d,spatial)
|
|
163
|
+
plt.show()
|
|
164
|
+
|
|
165
|
+
return np.array(domains).transpose()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#Currently broken
|
|
169
|
+
def point_click_multiscale(spatial,cluster_complex,clusterings,filt=0,order='persistence',redundant_filter=True):
|
|
170
|
+
from mpl_point_clicker import clicker
|
|
171
|
+
|
|
172
|
+
domains = map_multiscale(spatial,cluster_complex,clusterings,num_domains=0,filt=filt,plots='off',order=order,redundant_filter=redundant_filter)
|
|
173
|
+
tracker = []
|
|
174
|
+
exit = False
|
|
175
|
+
while not exit:
|
|
176
|
+
|
|
177
|
+
plt.figure(figsize=[15,15])
|
|
178
|
+
plt.rcParams.update({'font.size': 25})
|
|
179
|
+
ax = plt.gca()
|
|
180
|
+
ax.scatter(spatial[:,0],spatial[:,1],c='k')
|
|
181
|
+
ax.set_title('Click once to specify spot. Close to visualize domains')
|
|
182
|
+
ax.axes.xaxis.set_ticklabels([])
|
|
183
|
+
ax.axes.yaxis.set_ticklabels([])
|
|
184
|
+
klicker = clicker(ax, ["spot"], markers=["*"],markersize=20,colors=['red'])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
plt.tight_layout()
|
|
188
|
+
plt.show()
|
|
189
|
+
xy = klicker.get_positions()['spot']
|
|
190
|
+
|
|
191
|
+
input_ind = np.argmin((spatial[:,0]-xy[-1][0])**2+(spatial[:,1]-xy[-1][1])**2)
|
|
192
|
+
#print(input_ind)
|
|
193
|
+
input_cluster = list(i for i in range(len(clusterings)) if input_ind in clusterings[i])
|
|
194
|
+
#print(input_cluster)
|
|
195
|
+
x = spatial[:,0]
|
|
196
|
+
y = spatial[:,1]
|
|
197
|
+
|
|
198
|
+
for d in domains.transpose():
|
|
199
|
+
if d[input_ind] > .95:
|
|
200
|
+
tracker.append(d)
|
|
201
|
+
plot_multiscale(d,spatial,marker=xy[-1])
|
|
202
|
+
plt.show()
|
|
203
|
+
|
|
204
|
+
exit = input('Press E to exit.')
|
|
205
|
+
if exit == 'E':
|
|
206
|
+
return tracker
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def ground_truth_benchmark(ground_truth,multiscale,spatial,plots=False,conversion_factor=1):
|
|
210
|
+
|
|
211
|
+
ground_truth_distributions,gmat = clusters_to_distribution(ground_truth)
|
|
212
|
+
multiscale_distributions,mmat = clusters_to_distribution(multiscale)
|
|
213
|
+
|
|
214
|
+
dmat = ot.dist(spatial*conversion_factor,spatial*conversion_factor,metric='euclidean')
|
|
215
|
+
ma = np.max(dmat)
|
|
216
|
+
M = dmat/ma
|
|
217
|
+
optimal_costs = []
|
|
218
|
+
print('Wasserstein distance, index of optimal multiscale domain:')
|
|
219
|
+
for g in ground_truth_distributions:
|
|
220
|
+
g_cost = []
|
|
221
|
+
|
|
222
|
+
for m in multiscale_distributions:
|
|
223
|
+
|
|
224
|
+
d = ot.emd2(g,m,M)
|
|
225
|
+
g_cost.append(d)
|
|
226
|
+
optimal_costs.append((min(g_cost)*ma,np.argmin(g_cost)))
|
|
227
|
+
print((min(g_cost)*ma,int(np.argmin(g_cost))))
|
|
228
|
+
|
|
229
|
+
if plots:
|
|
230
|
+
for j in range(len(optimal_costs)):
|
|
231
|
+
plot_multiscale(gmat[:,j],spatial,title='Ground truth domain '+str(j))
|
|
232
|
+
plot_multiscale(mmat[:,optimal_costs[j][1]],spatial,title='Best match '+str(j))
|
|
233
|
+
plt.show()
|
|
234
|
+
return optimal_costs,ma
|
|
235
|
+
|
|
236
|
+
def construct_clustering(adata,domains):
|
|
237
|
+
category = np.zeros(adata.shape[0])
|
|
238
|
+
spatial = adata.obsm['spatial']
|
|
239
|
+
#Identify cell spots by the domain they most belong to.
|
|
240
|
+
for n in range(len(category)):
|
|
241
|
+
#Find the max scoring domain
|
|
242
|
+
arg_max = int(np.argmax(list(adata.obsm['multiscale'][n,domain] for domain in domains)))
|
|
243
|
+
#Exclude cells which don't belong to any domain
|
|
244
|
+
max_score = np.max(list(adata.obsm['multiscale'][n,domain] for domain in domains))
|
|
245
|
+
if max_score > 0.05:
|
|
246
|
+
category[n] = str(arg_max+1)
|
|
247
|
+
else:
|
|
248
|
+
category[n] = str(len(domains)+2)
|
|
249
|
+
|
|
250
|
+
# We want to get rid of unassigned spots
|
|
251
|
+
unassigned = list(n for n in range(len(category)) if category[n] == str(len(domains)+2))
|
|
252
|
+
new_spatial = spatial.copy()
|
|
253
|
+
new_spatial[unassigned,0] = 10**10
|
|
254
|
+
new_spatial[unassigned,1] = 10**10
|
|
255
|
+
for n in unassigned:
|
|
256
|
+
nearest_spot = np.argmin((new_spatial[:,0]-spatial[n,0])**2+(new_spatial[:,1]-spatial[n,1])**2)
|
|
257
|
+
category[n] = category[nearest_spot]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
plt.figure()
|
|
261
|
+
df = pd.DataFrame({"x":np.array(adata.obsm['spatial'][:,0]).flatten(),
|
|
262
|
+
"y":np.array(adata.obsm['spatial'][:,1]).flatten(),
|
|
263
|
+
"colors":np.array(category).flatten()})
|
|
264
|
+
cmap = plt.cm.Set1
|
|
265
|
+
norm = plt.Normalize(df['colors'].values.min(), df['colors'].values.max())
|
|
266
|
+
for i, dff in df.groupby("colors"):
|
|
267
|
+
plt.scatter(dff['x'], dff['y'], c=cmap(norm(dff['colors'])),
|
|
268
|
+
edgecolors='none', label="Feature {:g}".format(i))
|
|
269
|
+
|
|
270
|
+
plt.legend()
|
|
271
|
+
plt.show()
|
|
272
|
+
category = list(str(int(n)) for n in category)
|
|
273
|
+
#print(category)
|
|
274
|
+
return pd.Categorical(category,categories=list(str(i) for i in range(1,len(domains)+1)))
|
|
275
|
+
|
|
276
|
+
def plot_multiscale(multiscale,spatial,title='',marker=np.array([False])):
|
|
277
|
+
x = spatial[:,0]
|
|
278
|
+
y = spatial[:,1]
|
|
279
|
+
z = multiscale.copy()
|
|
280
|
+
z[z < .00002] = .00002
|
|
281
|
+
z[z > 1-.00002] = 1-.00002
|
|
282
|
+
|
|
283
|
+
plt.figure(figsize=(8, 20/3))
|
|
284
|
+
plt.title(title)
|
|
285
|
+
plt.scatter(x,y,c=logit(z),cmap='magma',s=30,edgecolors='k',linewidths=.5)
|
|
286
|
+
if np.any(marker):
|
|
287
|
+
plt.scatter(marker[0],marker[1],c='r',s=40,edgecolors='k',linewidths=.5,marker='*')
|
|
288
|
+
cbar = plt.colorbar()
|
|
289
|
+
cbar.ax.set_ylabel('logit(coreness)')
|
|
290
|
+
frame1 = plt.gca()
|
|
291
|
+
frame1.axes.xaxis.set_ticklabels([])
|
|
292
|
+
frame1.axes.yaxis.set_ticklabels([])
|
|
293
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phd-ms
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Identifying multiscale tissue domains for spatial transcriptomic data using persistent homology.
|
|
5
|
+
Author: Perry Beamer
|
|
6
|
+
License: MIT License
|
|
7
|
+
Keywords: multiscale domains,spatial transcriptomics,topological data analysis
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
14
|
+
Requires-Python: >=3.7
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE.md
|
|
17
|
+
Requires-Dist: gudhi>=3.8.0
|
|
18
|
+
Requires-Dist: leidenalg>=0.9.0
|
|
19
|
+
Requires-Dist: matplotlib==3.10.3
|
|
20
|
+
Requires-Dist: mpl_point_clicker
|
|
21
|
+
Requires-Dist: numpy>=1.23.4
|
|
22
|
+
Requires-Dist: pandas>=2.2.0
|
|
23
|
+
Requires-Dist: pot>=0.8.0
|
|
24
|
+
Requires-Dist: scanpy==1.9.6
|
|
25
|
+
Requires-Dist: scipy>=1.9.1
|
|
26
|
+
Requires-Dist: scikit-learn>=1.5.1
|
|
27
|
+
Requires-Dist: importlib-metadata
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# PHD-MS: Persistent Homology for Domains at Multiple Scales
|
|
32
|
+
|
|
33
|
+
Multiscale domain identification for spatial transcriptomic data.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Simply install with pip:
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
pip install phd-ms
|
|
42
|
+
|
|
43
|
+
Or install from source by downloading, cd to this directory, using:
|
|
44
|
+
|
|
45
|
+
pip install .
|
|
46
|
+
|
|
47
|
+
## Usage/Examples
|
|
48
|
+
|
|
49
|
+
Detailed jupyter notebook tutorials are available in the examples folder.
|
|
50
|
+
Or simply download the file titled 'point_and_click.py' and run the file to use our clickable graphical interface. When using the point_and_click.py interface, update the directory where your data is stored by changing this line:
|
|
51
|
+
```python
|
|
52
|
+
DATA = '/home/pbeamer/Documents/graphst/visium_hne_graphst'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Here we'll show a simple example with Visium DLPFC data, using default parameters.
|
|
56
|
+
First, import necessary components.
|
|
57
|
+
```python
|
|
58
|
+
import phd-ms
|
|
59
|
+
import scanpy
|
|
60
|
+
```
|
|
61
|
+
Preprocessing steps here (note that we assume a spatially-aware embedding has already been computed for your data):
|
|
62
|
+
```python
|
|
63
|
+
INPUT_FILE= '/home/pbeamer/Documents/graphst/adata_151673
|
|
64
|
+
phd_ms.tl.preprocess_leiden(INPUT_FILE,output_file=INPUT_FILE)
|
|
65
|
+
```
|
|
66
|
+
Compute persistent homology and plot 10 most prominent multiscale domains:
|
|
67
|
+
```python
|
|
68
|
+
cluster_complex,clusterings= phd_ms.tl.cluster_filtration(adata)
|
|
69
|
+
phd_ms.tl.map_multiscale(adata.obsm['spatial'],cluster_complex,clusterings,num_domains=10)
|
|
70
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
phd-ms/__init__.py,sha256=4eLPtR0jGYEKv5XFzeV6znhzTT4YY9qMyFharNe32HU,174
|
|
2
|
+
phd-ms/_utils/__init__.py,sha256=FiNfN0yjfM-r8K9puwb8tfv_2AaQu05Am7lPLxUj9wo,150
|
|
3
|
+
phd-ms/_utils/_benchmark.py,sha256=g4hhjNDt9volWpfMIwlknq0TuYehfVSBZmY-1jJCTxU,1156
|
|
4
|
+
phd-ms/_utils/_filtration.py,sha256=QDtshEVaua4QE8aD5arhoXkX6TTH0L1QGRxLSqwqTYU,5106
|
|
5
|
+
phd-ms/_utils/_leiden.py,sha256=KRwU7e1DrZ0AHEoHhMgWpgYnpFczUhSat9YRk4MbVXk,1655
|
|
6
|
+
phd-ms/tools/__init__.py,sha256=OtrsN0G41ij-4yzlL1ttOHbpRDO1sMBB4VVMHvlaRnk,275
|
|
7
|
+
phd-ms/tools/_phdms.py,sha256=K9FnPcRAthCEbgajCb0TV8lS5YvraPRBNqlYIC_l6-c,11645
|
|
8
|
+
phd_ms-0.0.0.dist-info/licenses/LICENSE.md,sha256=Zil4ZnX79GtBY8AHDwyKlzSjb75MOMMg9GXsYJMs434,1069
|
|
9
|
+
phd_ms-0.0.0.dist-info/METADATA,sha256=tN333bBq_Rtv6BvmHEP5LR2Sqo3wLhqcdnqteJR7sf8,2366
|
|
10
|
+
phd_ms-0.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
phd_ms-0.0.0.dist-info/top_level.txt,sha256=wr28LuzNsdR9Qwox6lNBOd-Q1hakdumUHcXy8n7bTrA,7
|
|
12
|
+
phd_ms-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Perry Beamer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
phd-ms
|