phd-ms 0.0.1__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 ADDED
@@ -0,0 +1,8 @@
1
+ # __init__ file
2
+ import warnings
3
+ warnings.filterwarnings('ignore')
4
+
5
+ from . import tools as tl
6
+
7
+ import sys
8
+ sys.modules.update({f'{__name__}.{m}': globals()[m] for m in ['tl']})
@@ -0,0 +1,3 @@
1
+ from ._leiden import leiden
2
+ from ._filtration import filt_to_matrix, union_find_dmat,get_sub_features
3
+ from ._benchmark import clusters_to_distribution
@@ -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
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,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: phd-ms
3
+ Version: 0.0.1
4
+ Summary: Domain identification for spatial transcriptomic data at multiple scales
5
+ Author: Perry Beamer
6
+ Author-email: Perry Beamer <perry.beamer@gmail.com>
7
+ License-Expression: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.md
12
+ Dynamic: license-file
13
+
14
+
15
+ # PHD-MS: Persistent Homology for Domains at Multiple Scales
16
+
17
+ Multiscale domain identification for spatial transcriptomic data.
18
+
19
+
20
+ ## Installation
21
+
22
+ Simply install with pip:
23
+
24
+
25
+ pip install phd-ms
26
+
27
+ Or install from source by downloading, cd to this directory, using:
28
+
29
+ pip install .
30
+
31
+ ## Usage/Examples
32
+
33
+ Detailed jupyter notebook tutorials are available in the examples folder.
34
+ 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:
35
+ ```python
36
+ DATA = '/home/pbeamer/Documents/graphst/visium_hne_graphst'
37
+ ```
38
+
39
+ Here we'll show a simple example with Visium DLPFC data, using default parameters.
40
+ First, import necessary components.
41
+ ```python
42
+ import phd-ms
43
+ import scanpy
44
+ ```
45
+ Preprocessing steps here (note that we assume a spatially-aware embedding has already been computed for your data):
46
+ ```python
47
+ INPUT_FILE= '/home/pbeamer/Documents/graphst/adata_151673
48
+ phd_ms.tl.preprocess_leiden(INPUT_FILE,output_file=INPUT_FILE)
49
+ ```
50
+ Compute persistent homology and plot 10 most prominent multiscale domains:
51
+ ```python
52
+ cluster_complex,clusterings= phd_ms.tl.cluster_filtration(adata)
53
+ phd_ms.tl.map_multiscale(adata.obsm['spatial'],cluster_complex,clusterings,num_domains=10)
54
+ ```
@@ -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.1.dist-info/licenses/LICENSE.md,sha256=Zil4ZnX79GtBY8AHDwyKlzSjb75MOMMg9GXsYJMs434,1069
9
+ phd_ms-0.0.1.dist-info/METADATA,sha256=QAaJpl-sgKYzAn3IKrV8jGbNPm55ZAa0q3ZwkfLuZTc,1736
10
+ phd_ms-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ phd_ms-0.0.1.dist-info/top_level.txt,sha256=32kNJtihZCqGCFLjUPxr8tQBX8wGOCgm8jhrZ7zZNTo,7
12
+ phd_ms-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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