pytrance 0.1.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.
pytrance/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """pyTrance: subcellular spatial transcriptomics analysis."""
2
+
3
+ from .cell_score import clq, clq_pairwise, clq_significance
4
+ from . import data
5
+ from .gnn import train_epoch
6
+ from .utils import sparse_mx_to_torch_sparse_tensor
7
+ from .models import DGI
8
+ from . import tools as tl
9
+ from . import plotting as pl
10
+
11
+ __all__ = [
12
+ "clq", "clq_pairwise", "clq_significance",
13
+ "CellData", "train_epoch", "get_neighbors",
14
+ "cluster_gene_embeddings_leiden",
15
+ "sparse_mx_to_torch_sparse_tensor",
16
+ "DGI",
17
+ ]
pytrance/cell_score.py ADDED
@@ -0,0 +1,489 @@
1
+ from itertools import combinations
2
+ from multiprocessing import Manager, Process
3
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
4
+
5
+ import numpy as np
6
+ from anndata import AnnData
7
+ from pandas import DataFrame
8
+ from scipy.sparse import spmatrix
9
+ from tqdm import tqdm
10
+
11
+ from .utils import get_neighbors
12
+
13
+
14
+ def clq_pairwise(
15
+ adata: AnnData,
16
+ genes: Sequence[Any],
17
+ graph: Optional[spmatrix] = None,
18
+ radius: float = 2,
19
+ n_neighbors: Optional[int] = None,
20
+ n_permutations: int = 0,
21
+ cell_key: str = "cell",
22
+ cat_key: str = "gene",
23
+ min_counts: int = 2,
24
+ n_processes: int = 1,
25
+ seed: int = 808,
26
+ **kwargs: Any,
27
+ ) -> Tuple[Dict, Dict]:
28
+ """Compute co-localization quotient (CLQ) scores between gene pairs.
29
+
30
+ Parameters
31
+ ----------
32
+ adata : AnnData
33
+ Annotated data object with transcript-level observations.
34
+ genes : array-like
35
+ List of gene names to compute pairwise CLQ for.
36
+ graph : sparse matrix, optional
37
+ Precomputed spatial neighbor graph. If None, computed from coordinates.
38
+ Default is None.
39
+ radius : float, optional
40
+ Spatial radius for neighbor graph construction. Default is 2.
41
+ n_neighbors : int, optional
42
+ Number of nearest neighbors if radius is None. Default is None.
43
+ n_permutations : int, optional
44
+ Number of permutations for score adjustment. Default is 0.
45
+ cell_key : str, optional
46
+ Column name in adata.obs for cell IDs. Default is "cell".
47
+ cat_key : str, optional
48
+ Column name in adata.obs for gene/category assignment. Default is "gene".
49
+ min_counts : int, optional
50
+ Minimum transcript count per gene per cell to include in analysis.
51
+ Default is 2.
52
+ n_processes : int, optional
53
+ Number of parallel processes for computation. Default is 1.
54
+ seed : int, optional
55
+ Random seed for reproducibility. Default is 808.
56
+ **kwargs
57
+ Additional keyword arguments passed to clq().
58
+
59
+ Returns
60
+ -------
61
+ tuple
62
+ - pairwise_clqs : dict
63
+ Dictionary mapping gene pairs to dictionaries containing 'clqs', 'clqs_adj',
64
+ and 'clqs_perm' (raw, adjusted, and permuted CLQ scores by cell).
65
+ - aggregated_norm_clqs : dict
66
+ Aggregated statistics with 'mean' and 'median' keys, mapping gene pairs
67
+ to aggregated normalized CLQ scores.
68
+ """
69
+
70
+ gene_pairs = list(combinations(genes, 2))
71
+ if radius is None: # otherwise clq is symmetric
72
+ gene_pairs.extend(
73
+ list(combinations(genes[::-1], 2))
74
+ ) # add pairs in reverse order
75
+ gene_pairs.extend([(g, g) for g in genes]) # add self pairs
76
+ pairwise_clqs = {}
77
+ for gene_pair in tqdm(gene_pairs, desc="gene pairs"):
78
+ # only keep cells with certain number of transcripts for both genes
79
+ adata_gene_pair = adata.copy()
80
+ for gene in gene_pair:
81
+ gene_cell_counts = (
82
+ adata_gene_pair[adata_gene_pair.obs[cat_key] == gene]
83
+ .obs[cell_key]
84
+ .value_counts()
85
+ )
86
+ gene_cell_counts_idxs = gene_cell_counts.index[
87
+ gene_cell_counts >= min_counts
88
+ ]
89
+ adata_gene_pair = adata_gene_pair[
90
+ adata_gene_pair.obs[cell_key].isin(gene_cell_counts_idxs)
91
+ ]
92
+
93
+ cell_clqs, cell_clqs_adjusted, cell_clqs_permuted = clq(
94
+ adata_gene_pair,
95
+ genes=gene_pair,
96
+ graph=graph,
97
+ n_neighbors=n_neighbors,
98
+ radius=radius,
99
+ n_permutations=n_permutations,
100
+ cell_key=cell_key,
101
+ cat_key=cat_key,
102
+ seed=seed,
103
+ pairwise=True,
104
+ verbose=0,
105
+ n_processes=n_processes,
106
+ **kwargs,
107
+ )
108
+ pairwise_clqs[gene_pair] = {
109
+ "clqs": cell_clqs,
110
+ "clqs_adj": cell_clqs_adjusted,
111
+ "clqs_perm": cell_clqs_permuted,
112
+ }
113
+
114
+ # for each pair aggregate (mean or median) normalized clq scores
115
+ aggregated_norm_clqs = {"mean": {}, "median": {}}
116
+ for pair, clq_dicts in pairwise_clqs.items():
117
+ clqs_adj_vals = list(clq_dicts["clqs_adj"].values())
118
+ aggregated_norm_clqs["median"][pair] = np.median(clqs_adj_vals)
119
+ aggregated_norm_clqs["mean"][pair] = np.mean(clqs_adj_vals)
120
+ if radius is not None: # symmetric
121
+ aggregated_norm_clqs["median"][(pair[1], pair[0])] = aggregated_norm_clqs[
122
+ "median"
123
+ ][pair]
124
+ aggregated_norm_clqs["mean"][(pair[1], pair[0])] = aggregated_norm_clqs[
125
+ "mean"
126
+ ][pair]
127
+
128
+ return pairwise_clqs, aggregated_norm_clqs
129
+
130
+
131
+ def clq(
132
+ adata: AnnData,
133
+ genes: Union[str, Sequence[Any]],
134
+ graph: Optional[spmatrix] = None,
135
+ radius: float = 2,
136
+ n_neighbors: Optional[int] = None,
137
+ n_permutations: int = 0,
138
+ n_processes: int = 1,
139
+ cell_key: str = "cell",
140
+ cat_key: str = "gene",
141
+ x_key: str = "x",
142
+ y_key: str = "y",
143
+ z_key: str = "z",
144
+ verbose: int = 1,
145
+ seed: int = 808,
146
+ **kwargs: Any,
147
+ ) -> Union[Dict, Tuple[Dict, Dict, Dict]]:
148
+ """Compute co-localization quotient (CLQ) scores across all cells.
149
+
150
+ Parameters
151
+ ----------
152
+ adata : AnnData
153
+ Annotated data object with transcript-level observations.
154
+ genes : str or array-like
155
+ Single gene name (str) for self co-localization or list of genes for
156
+ category definitions.
157
+ graph : sparse matrix, optional
158
+ Precomputed spatial neighbor graph. If None, computed from coordinates.
159
+ Default is None.
160
+ radius : float, optional
161
+ Spatial radius for neighbor graph construction. Default is 2.
162
+ n_neighbors : int, optional
163
+ Number of nearest neighbors if radius is None. Default is None.
164
+ n_permutations : int, optional
165
+ Number of permutations for score adjustment. Default is 0.
166
+ n_processes : int, optional
167
+ Number of parallel processes. Default is 1.
168
+ cell_key : str, optional
169
+ Column name in adata.obs for cell IDs. Default is "cell".
170
+ cat_key : str, optional
171
+ Column name in adata.obs for gene/category assignment. Default is "gene".
172
+ x_key : str, optional
173
+ Column name in adata.obs for x coordinates. Default is "x".
174
+ y_key : str, optional
175
+ Column name in adata.obs for y coordinates. Default is "y".
176
+ z_key : str, optional
177
+ Column name in adata.obs for z coordinates. Default is "z".
178
+ verbose : int, optional
179
+ Verbosity level (0=silent, 1+=with progress bars). Default is 1.
180
+ seed : int, optional
181
+ Random seed for permutation testing. Default is 808.
182
+ **kwargs
183
+ Additional keyword arguments passed to clq_single_cell().
184
+
185
+ Returns
186
+ -------
187
+ dict or tuple
188
+ If n_permutations > 0, returns tuple of:
189
+ - cell_clqs : dict - Raw CLQ scores per cell
190
+ - cell_clqs_adjusted : dict - Permutation-adjusted CLQ scores per cell
191
+ - cell_clqs_permuted : dict - Permutation distributions per cell
192
+
193
+ If n_permutations == 0, returns:
194
+ - cell_clqs : dict - Raw CLQ scores per cell
195
+ """
196
+
197
+ if radius is None and n_neighbors is None:
198
+ raise ValueError("either radius or n_neighbors must be specified")
199
+
200
+ if cat_key not in adata.obs.columns:
201
+ raise ValueError(f'"{cat_key}" not found in adata.obs')
202
+
203
+ if x_key not in adata.obs.columns:
204
+ raise ValueError(f'"{x_key}" not found in adata.obs')
205
+
206
+ if y_key not in adata.obs.columns:
207
+ raise ValueError(f'"{y_key}" not found in adata.obs')
208
+
209
+ if z_key and z_key not in adata.obs.columns:
210
+ raise ValueError(f'"{z_key}" not found in adata.obs')
211
+
212
+ categories = genes
213
+ if type(categories) is str: # single gene passed as string -> self co-localization
214
+ categories = [categories]
215
+ cat_a = categories
216
+ cat_b = categories
217
+
218
+ if n_permutations:
219
+ cell_clqs_permuted = {}
220
+
221
+ cell_ids = adata.obs[cell_key].unique()
222
+
223
+ # helper that processes a chunk of cells
224
+ def _worker(cells, shared_clqs, shared_norm, shared_perms, adata_sub):
225
+ # local copy of parameters to avoid closure issues
226
+ n_perm = n_permutations
227
+ grouped = adata_sub.obs.groupby(cell_key, observed=False)
228
+ for cell in tqdm(cells, desc="cells", disable=not bool(verbose)):
229
+ cell_df = grouped.get_group(cell)
230
+ if n_perm:
231
+ clq_val, clq_norm, clq_perms = clq_single_cell(
232
+ cell_df,
233
+ cat_a,
234
+ cat_b,
235
+ cat_key,
236
+ graph=graph,
237
+ radius=radius,
238
+ n_neighbors=n_neighbors,
239
+ n_permutations=n_perm,
240
+ x_key=x_key,
241
+ y_key=y_key,
242
+ z_key=z_key,
243
+ seed=seed,
244
+ **kwargs,
245
+ )
246
+ shared_norm[cell] = clq_norm
247
+ shared_perms[cell] = clq_perms
248
+ else: # no permutations case
249
+ clq_val = clq_single_cell(
250
+ cell_df,
251
+ cat_a,
252
+ cat_b,
253
+ cat_key,
254
+ graph=graph,
255
+ radius=radius,
256
+ n_neighbors=n_neighbors,
257
+ x_key=x_key,
258
+ y_key=y_key,
259
+ z_key=z_key,
260
+ seed=seed,
261
+ **kwargs,
262
+ )
263
+ shared_clqs[cell] = clq_val
264
+
265
+ # split list into n_processes chunks
266
+ cell_ids_split = [
267
+ cell_ids[i : i + max(1, len(cell_ids) // n_processes)]
268
+ for i in range(0, len(cell_ids), max(1, len(cell_ids) // n_processes))
269
+ ]
270
+
271
+ # prepare shared dicts
272
+ with Manager() as manager:
273
+ # manager = Manager()
274
+ cell_clqs = manager.dict()
275
+ cell_clqs_adjusted = manager.dict()
276
+ cell_clqs_permuted = manager.dict()
277
+
278
+ jobs = []
279
+ for sublist in cell_ids_split:
280
+ adata_sub = adata[adata.obs[cell_key].isin(sublist)]
281
+ if n_processes > 1:
282
+ p = Process(
283
+ target=_worker,
284
+ args=(
285
+ sublist,
286
+ cell_clqs,
287
+ cell_clqs_adjusted,
288
+ cell_clqs_permuted,
289
+ adata_sub,
290
+ ),
291
+ )
292
+ p.daemon = True
293
+ jobs.append(p)
294
+ p.start()
295
+ else:
296
+ # run in current process when only one worker
297
+ _worker(
298
+ sublist,
299
+ cell_clqs,
300
+ cell_clqs_adjusted,
301
+ cell_clqs_permuted,
302
+ adata_sub,
303
+ )
304
+
305
+ for proc in jobs:
306
+ proc.join()
307
+
308
+ if n_permutations:
309
+ return (
310
+ cell_clqs.copy(),
311
+ cell_clqs_adjusted.copy(),
312
+ cell_clqs_permuted.copy(),
313
+ )
314
+ else:
315
+ return cell_clqs.copy()
316
+
317
+
318
+ def clq_single_cell(
319
+ cell_df: DataFrame,
320
+ cat_a: Sequence[Any],
321
+ cat_b: Sequence[Any],
322
+ cat_key: str,
323
+ graph: Optional[spmatrix] = None,
324
+ radius: float = 2,
325
+ n_neighbors: Optional[int] = None,
326
+ n_permutations: Optional[int] = None,
327
+ x_key: str = "x",
328
+ y_key: str = "y",
329
+ z_key: str = "z",
330
+ seed: int = 808,
331
+ **kwargs: Any,
332
+ ) -> Union[float, Tuple[float, float, List]]:
333
+ """Compute co-localization quotient (CLQ) for a single cell.
334
+
335
+ Parameters
336
+ ----------
337
+ cell_df : DataFrame
338
+ Cell-specific transcript data.
339
+ cat_a : array-like
340
+ First gene category (list of gene names).
341
+ cat_b : array-like
342
+ Second gene category (list of gene names).
343
+ cat_key : str
344
+ Column name in cell_df for gene/category assignment.
345
+ graph : sparse matrix, optional
346
+ Precomputed spatial neighbor graph. If None, constructed from coordinates.
347
+ Default is None.
348
+ radius : float, optional
349
+ Spatial radius for neighbor graph construction. Default is 2.
350
+ n_neighbors : int, optional
351
+ Number of nearest neighbors if radius is None. Default is None.
352
+ n_permutations : int, optional
353
+ Number of permutations for normalized CLQ computation. Default is None.
354
+ x_key : str, optional
355
+ Column name in cell_df for x coordinates. Default is "x".
356
+ y_key : str, optional
357
+ Column name in cell_df for y coordinates. Default is "y".
358
+ z_key : str, optional
359
+ Column name in cell_df for z coordinates. Default is "z".
360
+ seed : int, optional
361
+ Random seed for permutation testing. Default is 808.
362
+ **kwargs
363
+ Additional keyword arguments passed to get_neighbors().
364
+
365
+ Returns
366
+ -------
367
+ float or tuple
368
+ If n_permutations is None or 0:
369
+ - clq : float - Raw CLQ score
370
+
371
+ If n_permutations > 0:
372
+ - clq : float - Raw CLQ score
373
+ - clq_adjusted : float - Permutation-normalized CLQ score
374
+ - clq_perms : list - Distribution of CLQ scores from permutations
375
+ """
376
+
377
+ n_a = (cell_df[cat_key].isin(cat_a)).sum()
378
+ if cat_a == cat_b:
379
+ n_b = n_a - 1
380
+ else:
381
+ n_b = (cell_df[cat_key].isin(cat_b)).sum()
382
+ if n_a == 0 or n_b == 0: # avoid 0 division
383
+ if n_permutations:
384
+ return 0, np.nan, [0]
385
+ else:
386
+ return 0
387
+
388
+ if graph is None: # build graph from scratch if not provided
389
+ graph = get_neighbors(
390
+ cell_df,
391
+ radius=radius,
392
+ n_neighbors=n_neighbors,
393
+ x_key=x_key,
394
+ y_key=y_key,
395
+ z_key=z_key,
396
+ n_jobs=1, # disable parallel in multiprocessing context
397
+ )
398
+ else: # in case provided graph includes not only given cell
399
+ graph = graph[np.ix_(cell_df.index.astype(int), cell_df.index.astype(int))]
400
+
401
+ cell_df.reset_index(drop=True, inplace=True) # to ensure index matches graph size
402
+ c_ab = graph[
403
+ np.ix_(
404
+ cell_df[cell_df[cat_key].isin(cat_a)].index,
405
+ cell_df[cell_df[cat_key].isin(cat_b)].index,
406
+ )
407
+ ].sum()
408
+
409
+ n = cell_df.shape[0]
410
+ clq = (c_ab / n_a) / (n_b / (n - 1))
411
+
412
+ if n_permutations is None or n_permutations == 0:
413
+ return clq
414
+
415
+ # permute to have control for score
416
+ else:
417
+ rng = np.random.default_rng(seed)
418
+ clq_perms = []
419
+ for p in range(n_permutations):
420
+ # random shuffling of gene labels
421
+ perm_idx = rng.permutation(cell_df.shape[0])
422
+ permuted_categories = cell_df[cat_key].copy()
423
+ permuted_categories = np.asarray(permuted_categories)[perm_idx]
424
+ cell_df_permuted = cell_df.copy()
425
+ cell_df_permuted[cat_key] = permuted_categories
426
+
427
+ # only recompute c_ab, other values are same as unpermuted
428
+ c_ab_permuted = graph[
429
+ np.ix_(
430
+ cell_df_permuted[cell_df_permuted[cat_key].isin(cat_a)].index,
431
+ cell_df_permuted[cell_df_permuted[cat_key].isin(cat_b)].index,
432
+ )
433
+ ].sum()
434
+
435
+ clq_perms.append((c_ab_permuted / n_a) / (n_b / (n - 1)))
436
+
437
+ if np.std(clq_perms) == 0 and np.mean(clq_perms) == 0:
438
+ clq_adjusted = clq
439
+ elif np.std(clq_perms) == 0 and np.mean(clq_perms) == clq:
440
+ clq_adjusted = 0
441
+ else:
442
+ clq_adjusted = clq / np.mean(clq_perms)
443
+
444
+ return clq, clq_adjusted, clq_perms
445
+
446
+
447
+ def clq_significance(
448
+ cell_clqs: Dict,
449
+ cell_clqs_permuted: Dict,
450
+ percentile: float = 5,
451
+ ) -> Tuple[List, Dict]:
452
+ """Assess statistical significance of CLQ scores using permutation distributions.
453
+
454
+ Parameters
455
+ ----------
456
+ cell_clqs : dict
457
+ Observed raw CLQ per cell, mapping cell IDs to float values.
458
+ cell_clqs_permuted : dict
459
+ Permutation distributions per cell, mapping cell IDs to lists of
460
+ CLQs from permutations.
461
+ percentile : float, optional
462
+ Percentile threshold for significance testing. A cell is significant if
463
+ its observed CLQ is beyond the [percentile, 100-percentile] range.
464
+ Default is 5.
465
+
466
+ Returns
467
+ -------
468
+ tuple
469
+ - significant_clq_cells : list - Cell IDs with significant CLQ scores
470
+ - observed_vs_percentile : dict - Fold-change between observed CLQ and
471
+ the nearest percentile threshold for each cell (1.0 for non-significant)
472
+ """
473
+
474
+ observed_vs_percentile = {}
475
+ significant_clq_cells = []
476
+ for cell, clqs in cell_clqs_permuted.items():
477
+ lower_percentile = np.percentile(clqs, percentile)
478
+ upper_percentile = np.percentile(clqs, 100 - percentile)
479
+ observed_clq = cell_clqs[cell]
480
+ if observed_clq < lower_percentile:
481
+ observed_vs_percentile[cell] = lower_percentile / observed_clq
482
+ significant_clq_cells.append(cell)
483
+ elif observed_clq > upper_percentile:
484
+ observed_vs_percentile[cell] = observed_clq / upper_percentile
485
+ significant_clq_cells.append(cell)
486
+ else:
487
+ observed_vs_percentile[cell] = 1
488
+
489
+ return significant_clq_cells, observed_vs_percentile
@@ -0,0 +1,5 @@
1
+ """Data handling module for pyTrance."""
2
+
3
+ from .celldata import CellData
4
+
5
+ __all__ = ["CellData"]
@@ -0,0 +1,144 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ import numpy as np
4
+ from anndata import AnnData
5
+ from scipy import sparse as sp
6
+ from torch import from_numpy, long, tensor
7
+ from torch.utils import data
8
+ from torch_geometric.data import Data
9
+ from tqdm import tqdm
10
+
11
+ from ..utils import get_neighbors
12
+
13
+
14
+ class CellData(data.Dataset):
15
+ """PyTorch Dataset for cell-level graph data of transcripts.
16
+
17
+ Parameters
18
+ ----------
19
+ adata : AnnData
20
+ Annotated data object with transcript-level observations and features.
21
+ cell_indices : dict
22
+ Dictionary mapping cell IDs to arrays of transcript indices for each cell.
23
+ graph_kwargs : dict, optional
24
+ Keyword arguments for neighbor graph construction passed to get_neighbors().
25
+ Default is None.
26
+ adj : sparse matrix, optional
27
+ Precomputed adjacency matrix. If None, compute per-cell from coordinates.
28
+ Default is None.
29
+ corruption : {'feature_shuffling'}, optional
30
+ Corruption strategy for negative samples in contrastive learning.
31
+ Default is "feature_shuffling".
32
+ seed : int, optional
33
+ Random seed for reproducibility. Default is 808.
34
+
35
+ Attributes
36
+ ----------
37
+ data : list
38
+ List of torch_geometric.data.Data objects, one per cell.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ adata: AnnData,
44
+ cell_indices: Dict,
45
+ graph_kwargs: Optional[Dict] = None,
46
+ adj: Optional[sp.spmatrix] = None,
47
+ corruption: str = "feature_shuffling",
48
+ seed: int = 808,
49
+ ) -> None:
50
+ self.data = self.__partition__(
51
+ adata, cell_indices, graph_kwargs, adj, corruption, seed
52
+ )
53
+
54
+ def __partition__(
55
+ self,
56
+ adata: AnnData,
57
+ cell_indices: Dict,
58
+ graph_kwargs: Optional[Dict],
59
+ adj: Optional[sp.spmatrix],
60
+ corruption: str,
61
+ seed: int,
62
+ ) -> List[Data]:
63
+ """Partition data into per-cell graph objects with features and corruption.
64
+
65
+ Parameters
66
+ ----------
67
+ adata : AnnData
68
+ Annotated data object with transcript-level observations and features.
69
+ cell_indices : dict
70
+ Dictionary mapping cell IDs to arrays of transcript indices for each cell.
71
+ graph_kwargs : dict
72
+ Keyword arguments for neighbor graph construction passed to get_neighbors().
73
+ adj : sparse matrix or None
74
+ Precomputed global adjacency matrix. If None, compute per-cell from coordinates.
75
+ corruption : str
76
+ Feature corruption strategy.
77
+ seed : int
78
+ Random seed for reproducibility.
79
+
80
+ Returns
81
+ -------
82
+ list
83
+ List of torch_geometric.data.Data objects with graph, features, and
84
+ corrupted features for each cell.
85
+ """
86
+
87
+ data_cells = []
88
+ rng = np.random.default_rng(seed)
89
+ print("setting up torch data set ...")
90
+ for node_idxs in tqdm(cell_indices.values()):
91
+ # construct feature tensor
92
+ cell_adata = adata[node_idxs]
93
+ cell_features_transcripts = cell_adata.X
94
+
95
+ # Convert IDs to sparse format
96
+ cell_ids = cell_adata.obs.cell_encoded.values[np.newaxis].T
97
+ spot_ids = node_idxs[np.newaxis].T
98
+ cell_ids_sparse = sp.csr_matrix(cell_ids)
99
+ spot_ids_sparse = sp.csr_matrix(spot_ids)
100
+
101
+ # Concatenate
102
+ cell_features = sp.hstack(
103
+ [cell_features_transcripts, cell_ids_sparse, spot_ids_sparse]
104
+ )
105
+ cell_features = from_numpy(cell_features.toarray()).to_sparse()
106
+
107
+ # permute features
108
+ if corruption == "feature_shuffling":
109
+ perm_idxs_cell = rng.permutation(np.arange(cell_adata.n_obs))
110
+ corr_fts = cell_features_transcripts[perm_idxs_cell, :]
111
+ else:
112
+ raise ValueError(f"Corruption method '{corruption}' not implemented")
113
+
114
+ corr_fts = from_numpy(corr_fts.toarray()).to_sparse()
115
+
116
+ if adj is not None: # use provided graph
117
+ adj_cell = adj[np.ix_(node_idxs, node_idxs)]
118
+
119
+ else: # compute graph from scratch
120
+ adj_cell = get_neighbors(cell_adata.obs, **graph_kwargs)
121
+
122
+ # convert adjacency matrix to sparse tensor
123
+ if sp.issparse(adj_cell):
124
+ adj_cell = adj_cell.tocoo()
125
+ else:
126
+ adj_cell = sp.coo_matrix(adj_cell)
127
+ adj_cell = tensor(
128
+ np.vstack((adj_cell.row, adj_cell.col)), dtype=long, device="cpu"
129
+ )
130
+
131
+ data_cells.append(
132
+ Data(x=cell_features, edge_index=adj_cell, corr_fts=corr_fts)
133
+ )
134
+ return data_cells
135
+
136
+ def __len__(self):
137
+ return len(self.data)
138
+
139
+ def __getitem__(self, index):
140
+ out = self.data[index]
141
+ return out
142
+
143
+ def __repr__(self) -> str:
144
+ return f"{self.__class__.__name__}"