phd-ms 1.0__tar.gz
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-1.0/PKG-INFO +34 -0
- phd_ms-1.0/README.md +2 -0
- phd_ms-1.0/phd_ms/__init__.py +8 -0
- phd_ms-1.0/phd_ms/_utils/__init__.py +3 -0
- phd_ms-1.0/phd_ms/_utils/_benchmark.py +28 -0
- phd_ms-1.0/phd_ms/_utils/_filtration.py +141 -0
- phd_ms-1.0/phd_ms/_utils/_leiden.py +41 -0
- phd_ms-1.0/phd_ms/tools/__init__.py +8 -0
- phd_ms-1.0/phd_ms/tools/_phdms.py +293 -0
- phd_ms-1.0/phd_ms.egg-info/PKG-INFO +34 -0
- phd_ms-1.0/phd_ms.egg-info/SOURCES.txt +16 -0
- phd_ms-1.0/phd_ms.egg-info/dependency_links.txt +1 -0
- phd_ms-1.0/phd_ms.egg-info/not-zip-safe +1 -0
- phd_ms-1.0/phd_ms.egg-info/requires.txt +11 -0
- phd_ms-1.0/phd_ms.egg-info/top_level.txt +1 -0
- phd_ms-1.0/setup.cfg +45 -0
- phd_ms-1.0/setup.py +10 -0
phd_ms-1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phd-ms
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Multiscale domain identification for spatial transcriptomic data.
|
|
5
|
+
Author: Perry Beamer
|
|
6
|
+
Author-email: perry.beamer@gmail.com
|
|
7
|
+
License: MIT License
|
|
8
|
+
Keywords: spatial transcriptomics,topological data analysis,multiscale domains
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: gudhi>=3.8.0
|
|
19
|
+
Requires-Dist: leidenalg>=0.9.0
|
|
20
|
+
Requires-Dist: matplotlib==3.8.2
|
|
21
|
+
Requires-Dist: numpy>=1.23.4
|
|
22
|
+
Requires-Dist: pandas>=2.2.0
|
|
23
|
+
Requires-Dist: plotly>=5.3.1
|
|
24
|
+
Requires-Dist: pot>=0.8.0
|
|
25
|
+
Requires-Dist: scanpy==1.9.6
|
|
26
|
+
Requires-Dist: scipy>=1.9.1
|
|
27
|
+
Requires-Dist: scikit-learn>=1.5.1
|
|
28
|
+
Requires-Dist: importlib-metadata
|
|
29
|
+
Dynamic: author
|
|
30
|
+
Dynamic: author-email
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# phd-ms00.03
|
|
34
|
+
|
phd_ms-1.0/README.md
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
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phd-ms
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Multiscale domain identification for spatial transcriptomic data.
|
|
5
|
+
Author: Perry Beamer
|
|
6
|
+
Author-email: perry.beamer@gmail.com
|
|
7
|
+
License: MIT License
|
|
8
|
+
Keywords: spatial transcriptomics,topological data analysis,multiscale domains
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: gudhi>=3.8.0
|
|
19
|
+
Requires-Dist: leidenalg>=0.9.0
|
|
20
|
+
Requires-Dist: matplotlib==3.8.2
|
|
21
|
+
Requires-Dist: numpy>=1.23.4
|
|
22
|
+
Requires-Dist: pandas>=2.2.0
|
|
23
|
+
Requires-Dist: plotly>=5.3.1
|
|
24
|
+
Requires-Dist: pot>=0.8.0
|
|
25
|
+
Requires-Dist: scanpy==1.9.6
|
|
26
|
+
Requires-Dist: scipy>=1.9.1
|
|
27
|
+
Requires-Dist: scikit-learn>=1.5.1
|
|
28
|
+
Requires-Dist: importlib-metadata
|
|
29
|
+
Dynamic: author
|
|
30
|
+
Dynamic: author-email
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# phd-ms00.03
|
|
34
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.cfg
|
|
3
|
+
setup.py
|
|
4
|
+
phd_ms/__init__.py
|
|
5
|
+
phd_ms.egg-info/PKG-INFO
|
|
6
|
+
phd_ms.egg-info/SOURCES.txt
|
|
7
|
+
phd_ms.egg-info/dependency_links.txt
|
|
8
|
+
phd_ms.egg-info/not-zip-safe
|
|
9
|
+
phd_ms.egg-info/requires.txt
|
|
10
|
+
phd_ms.egg-info/top_level.txt
|
|
11
|
+
phd_ms/_utils/__init__.py
|
|
12
|
+
phd_ms/_utils/_benchmark.py
|
|
13
|
+
phd_ms/_utils/_filtration.py
|
|
14
|
+
phd_ms/_utils/_leiden.py
|
|
15
|
+
phd_ms/tools/__init__.py
|
|
16
|
+
phd_ms/tools/_phdms.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
phd_ms
|
phd_ms-1.0/setup.cfg
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = phd-ms
|
|
3
|
+
version = 0.1
|
|
4
|
+
author = Perry Beamer
|
|
5
|
+
author_email = perry.beamer@gmail.com
|
|
6
|
+
description = Multiscale domain identification for spatial transcriptomic data using persistent homology
|
|
7
|
+
long_description_content_type = text/markdown
|
|
8
|
+
long_description = file: README.md
|
|
9
|
+
license = MIT License
|
|
10
|
+
classifiers =
|
|
11
|
+
Programming Language :: Python :: 3.8
|
|
12
|
+
Programming Language :: Python :: 3.9
|
|
13
|
+
License :: OSI Approved :: MIT License
|
|
14
|
+
Operating System :: POSIX :: Linux
|
|
15
|
+
Operating System :: MacOS
|
|
16
|
+
Topic :: Scientific/Engineering
|
|
17
|
+
Topic :: Scientific/Engineering :: Bio-Informatics
|
|
18
|
+
keywords = spatial transcriptomics, topological data analysis, multiscale domains
|
|
19
|
+
|
|
20
|
+
[options]
|
|
21
|
+
zip_safe = False
|
|
22
|
+
include_package_data = True
|
|
23
|
+
packages = find:
|
|
24
|
+
install_requires =
|
|
25
|
+
gudhi>=3.8.0
|
|
26
|
+
leidenalg>=0.9.0
|
|
27
|
+
matplotlib==3.8.2
|
|
28
|
+
numpy>=1.23.4
|
|
29
|
+
pandas>=2.2.0
|
|
30
|
+
plotly>=5.3.1
|
|
31
|
+
pot>=0.8.0
|
|
32
|
+
scanpy==1.9.6
|
|
33
|
+
scipy>=1.9.1
|
|
34
|
+
scikit-learn>=1.5.1
|
|
35
|
+
importlib-metadata
|
|
36
|
+
python_requires = >=3.9
|
|
37
|
+
|
|
38
|
+
[options.packages.find]
|
|
39
|
+
exclude =
|
|
40
|
+
tutorials*
|
|
41
|
+
|
|
42
|
+
[egg_info]
|
|
43
|
+
tag_build =
|
|
44
|
+
tag_date = 0
|
|
45
|
+
|
phd_ms-1.0/setup.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='phd-ms',
|
|
5
|
+
version='1.0',
|
|
6
|
+
packages=find_packages(exclude=['tests*']),
|
|
7
|
+
description='Multiscale domain identification for spatial transcriptomic data.',
|
|
8
|
+
author='Perry Beamer',
|
|
9
|
+
author_email='perry.beamer@gmail.com'
|
|
10
|
+
)
|