spatialrna 0.1.2__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.
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.2
2
+ Name: spatialrna
3
+ Version: 0.1.2
4
+ Summary: A package for spatial RNA graph data processing and analysis.
5
+ Author-email: Ruqian Lyu <rlyu@svi.edu.au>
6
+ License: MIT
7
+ Project-URL: repository, https://gitlab.svi.edu.au/biocellgen-private/spatialrna
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: torch
14
+ Requires-Dist: torch_geometric
15
+ Requires-Dist: pandas
16
+ Requires-Dist: numpy
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=6.0; extra == "dev"
19
+ Requires-Dist: twine>=4.0.2; extra == "dev"
20
+ Requires-Dist: build; extra == "dev"
21
+
22
+ ## A package that provides easier inferface to GNN models for single-molecule ST data
23
+
24
+
25
+
26
+ ## Pre-requisite packages installation
27
+
28
+ ```
29
+ ## Using Conda and pip
30
+
31
+ conda create -n dev-spatialrna
32
+ conda activate dev-spatialrna
33
+ conda install python=3.10.0
34
+
35
+ ## torch 2.4.0 cuda version 12.4
36
+ pip install torch==2.4.0 --index-url https://download.pytorch.org/whl/cu124
37
+
38
+ ## torch_geometric=2.6.1 https://pytorch-geometric.readthedocs.io/en/stable/install/installation.html#installation-via-pypi
39
+
40
+ pip install torch_geometric==2.6.1
41
+
42
+ ## Additional useful libraries https://pytorch-geometric.readthedocs.io/en/stable/install/installation.html#additional-libraries
43
+
44
+ pip install pyg_lib torch_scatter torch_sparse torch_cluster torch_spline_conv -f https://data.pyg.org/whl/torch-2.4.0+cu124.html
45
+
46
+ ```
47
+
48
+ ## Install spatialrna
49
+
50
+ ```
51
+ pip install spatialrna
52
+ ```
@@ -0,0 +1,31 @@
1
+ ## A package that provides easier inferface to GNN models for single-molecule ST data
2
+
3
+
4
+
5
+ ## Pre-requisite packages installation
6
+
7
+ ```
8
+ ## Using Conda and pip
9
+
10
+ conda create -n dev-spatialrna
11
+ conda activate dev-spatialrna
12
+ conda install python=3.10.0
13
+
14
+ ## torch 2.4.0 cuda version 12.4
15
+ pip install torch==2.4.0 --index-url https://download.pytorch.org/whl/cu124
16
+
17
+ ## torch_geometric=2.6.1 https://pytorch-geometric.readthedocs.io/en/stable/install/installation.html#installation-via-pypi
18
+
19
+ pip install torch_geometric==2.6.1
20
+
21
+ ## Additional useful libraries https://pytorch-geometric.readthedocs.io/en/stable/install/installation.html#additional-libraries
22
+
23
+ pip install pyg_lib torch_scatter torch_sparse torch_cluster torch_spline_conv -f https://data.pyg.org/whl/torch-2.4.0+cu124.html
24
+
25
+ ```
26
+
27
+ ## Install spatialrna
28
+
29
+ ```
30
+ pip install spatialrna
31
+ ```
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "spatialrna"
7
+ version = "0.1.2"
8
+ description="A package for spatial RNA graph data processing and analysis."
9
+ readme = {file = "README.md", content-type = "text/markdown"}
10
+ authors = [
11
+ {name = "Ruqian Lyu", email = "rlyu@svi.edu.au"},
12
+ ]
13
+ license = {text = "MIT"}
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ dependencies = [
21
+ "torch",
22
+ "torch_geometric",
23
+ "pandas",
24
+ "numpy"
25
+ ]
26
+ requires-python = ">=3.10"
27
+
28
+ [tool.setuptools]
29
+ include-package-data = false
30
+
31
+ [tool.setuptools.packages.find]
32
+ exclude = ["data", "tests"]
33
+ include = ["spatialrna"]
34
+
35
+
36
+ # BETA-feature "Automatic discovery"
37
+ # Leaving out any information about the package structure to enable automatic discovery. src-layout works out of the box
38
+ # https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#automatic-discovery
39
+
40
+ [project.urls]
41
+ repository = "https://gitlab.svi.edu.au/biocellgen-private/spatialrna"
42
+
43
+ [project.optional-dependencies]
44
+ dev = [
45
+ "pytest>=6.0",
46
+ "twine>=4.0.2",
47
+ "build",
48
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ from .spatialrna import SpatialRNA
@@ -0,0 +1,335 @@
1
+ from torch_geometric.data import InMemoryDataset, Dataset, Data
2
+ from typing import Callable, Optional, List
3
+ from torch_geometric.nn import radius_graph
4
+ import os
5
+ import os.path as osp
6
+ import pandas as pd
7
+ import torch
8
+ from torch_geometric.io import fs
9
+ from torch_geometric.loader import LinkNeighborLoader
10
+
11
+
12
+
13
+ ## feature matrix. Reduce mem and I/O by mapping gene label to int IDs. Delay mapping to 1-D tensors when training.
14
+ ## TODO: warnings about different num_tiles, radius_r arguments if only loading not processing
15
+
16
+ def files_exist(files: List[str]) -> bool:
17
+ # NOTE: We return `False` in case `files` is empty, leading to a
18
+ # re-processing of files on every instantiation.
19
+ return len(files) != 0 and all([fs.exists(f) for f in files])
20
+
21
+ class SpatialRNA(InMemoryDataset):
22
+ r"""A spatial RNA graph dataset. One dataset per tissue sample, and optionally creates
23
+ tiles from a tissue area. For each generated data.pt, it contains the nodes in core areas for
24
+ making inference for transcripts with complete neighbourhoods.
25
+
26
+
27
+ Args:
28
+ root (str): Root directory where the dataset should be saved.
29
+ sample_name (str): The name of this sample. Appended to the processed [sample_name]_data.pt.
30
+ Using basename of root dir if not supplied.
31
+ radius_r (float): The radius for building RNA graphs.
32
+ tile_by_dim (str): Along which dimension to make tiles. It should be a column in the transcript data.frame.
33
+ num_tiles (int): The number of tiles to create from this tissue sample.
34
+ process_mode (str): The mode of process can be either "tile" or "subgraph".
35
+ load_type (str): If :obj:`"tile"`, load the tile graph data.
36
+ If :obj:`"subgraph"`, load the tile subgraph data.
37
+ load_tile_id (int): Select one tile to load.
38
+ process_tile_ids (List[int]): Select tiles to process including generating tile graph and subgraph.
39
+ one_hot_encoding (dict): Dictionary that mapps gene names to integer (or one-hot-encoding tensors).
40
+ It is essential to apply the same dictionary for all samples that are analyzed together.
41
+ transform (callable, optional): A function/transform that takes in an
42
+ :obj:`torch_geometric.data.Data` object and returns a transformed
43
+ version. The data object will be transformed before every access.
44
+ (default: :obj:`None`)
45
+ pre_transform (callable, optional): A function/transform that takes in
46
+ an :obj:`torch_geometric.data.Data` object and returns a
47
+ transformed version. The data object will be transformed before
48
+ being saved to disk. (default: :obj:`None`)
49
+ force_reload (bool, optional): Whether to re-process the dataset.
50
+ (default: :obj:`False`)
51
+ force_resample (bool, optional): Whether to re-process the tile dataset to generate subgraphs.
52
+ (default: :obj:`False`)
53
+ subgraph_mode (str): The mode of subgraph generation can be either "link_based" or "node_based".
54
+ (default: :obj:`link_based`)
55
+ num_sampled_edges (int): sample this many edge_index as positive edges when generating subgraph from a tile (used in LinkNeighborLoader).
56
+ num_seed_nodes:(int, optional): Number of seed nodes for generating subgraphs.
57
+ (default: :obj:`5000`)
58
+ num_walks:(int, optional): Number of walks for generating subgraphs.
59
+ (default: :obj:`5`)
60
+
61
+ log (bool, optional): Whether to print any console output while
62
+ downloading and processing the dataset. (default: :obj:`True`)
63
+ **kwargs (optional): Additional arguments of
64
+ :class:`torch.utils.data.LinkNeighborLoader`, such as :obj:`num_neighbors`
65
+ """
66
+ def __init__(
67
+ self,
68
+ root: str,
69
+ sample_name: Optional[str] = None,
70
+ radius_r: float = 3.0,
71
+ dim_x: str = "X",
72
+ dim_y: str = "Y",
73
+ tile_by_dim: str = "Y",
74
+ num_tiles: int = 1,
75
+ process_mode: str = "tile",
76
+ load_type: str = "tile",
77
+ load_tile_id: int = 0,
78
+ pad_hops: float = 2,
79
+ feature_col = "gene",
80
+ max_num_neighbors: int = 500,
81
+ process_tile_ids: List[int] = None,
82
+ one_hot_encoding: dict = None,
83
+ subgraph_mode: str = "link_based",
84
+ num_sampled_edges: int = 5000,
85
+ num_seed_nodes: int = 5000,
86
+ num_walks: int = 5,
87
+ transform = None,
88
+ pre_transform = None,
89
+ force_reload: bool = False,
90
+ force_resample: bool = False,
91
+ log: bool= True,
92
+ seed: int = 100,
93
+ **kwargs,
94
+
95
+ )-> None:
96
+ assert load_type in ["tile","subgraph","blank"]
97
+ assert process_mode in ["tile", "subgraph"]
98
+ assert subgraph_mode in ["link_based", "node_based"]
99
+
100
+ self.num_tiles = num_tiles
101
+ if sample_name is not None:
102
+ self.sample_name = sample_name
103
+ else:
104
+ self.sample_name = osp.basename(root)
105
+
106
+ self.radius_r = radius_r
107
+ self.load_tile_id = load_tile_id
108
+ self.pad_hops = pad_hops
109
+ self.max_num_neighbors = max_num_neighbors
110
+ self.one_hot_encoding = one_hot_encoding
111
+ self.seed = seed
112
+ self.feature_col = feature_col
113
+ self.num_sampled_edges = num_sampled_edges
114
+ self.load_type = load_type
115
+ self.process_mode = process_mode
116
+ self.num_seed_nodes = num_seed_nodes
117
+ self.num_walks = num_walks
118
+
119
+ assert tile_by_dim in [dim_x,dim_y]
120
+ assert one_hot_encoding is not None
121
+
122
+ self.dim_x = dim_x
123
+ self.dim_y = dim_y
124
+ self.tile_by_dim = tile_by_dim
125
+ self.log = log
126
+ self.force_resample = force_resample
127
+ self.subgraph_mode = subgraph_mode
128
+
129
+ if process_tile_ids is None:
130
+ self.process_tile_ids = [load_tile_id]
131
+ else:
132
+ self.process_tile_ids = process_tile_ids
133
+ # super init, process tiles, if tiles.pt not exist, and regenerate if force_reload=True.
134
+ super().__init__(root, transform, pre_transform,force_reload=force_reload, log=log)
135
+ # subsequently generates the subgraphs if requested
136
+ if process_mode == "subgraph":
137
+ self.generate_subgraph(**kwargs)
138
+
139
+ if load_type == 'subgraph':
140
+ load_file_name = osp.join(self.subgraph_dir,f'{self.sample_name}_subgraph_data_tile{self.load_tile_id}.pt')
141
+ # if self.force_resample or not files_exist([load_file_name]):
142
+ # self.generate_subgraph(**kwargs)
143
+ elif load_type == 'tile':
144
+ load_file_name = osp.join(self.processed_dir,f'{self.sample_name}_data_tile{self.load_tile_id}.pt')
145
+ else:
146
+ load_file_name = None
147
+
148
+ if load_file_name is None:
149
+ print(load_file_name," not exist, nothing loaded")
150
+ elif files_exist([load_file_name]):
151
+ print("loading from file ",load_file_name)
152
+ self.load(load_file_name)
153
+ else:
154
+ print(load_file_name," not exist, nothing loaded")
155
+ None
156
+
157
+
158
+ @property
159
+ def subgraph_dir(self) -> str:
160
+ return osp.join(self.root,"subgraph")
161
+
162
+ @property
163
+ def raw_file_names(self):
164
+ return [f'{self.sample_name}.csv']
165
+
166
+ @property
167
+ def subgraph_file_names(self):
168
+ return [f'{self.sample_name}_subgraph_data_tile{tile}.pt' for tile in self.process_tile_ids]
169
+
170
+ @property
171
+ def processed_file_names(self):
172
+ return [f'{self.sample_name}_data_tile{tile}.pt' for tile in self.process_tile_ids]
173
+
174
+ @property
175
+ def subgraph_paths(self) -> List[str]:
176
+ r"""The absolute filepaths for generated subgraph data from each sample tile
177
+ """
178
+ files = self.subgraph_file_names
179
+ # Prevent a common source of error in which `file_names` are not
180
+ # defined as a property.
181
+ if isinstance(files, Callable):
182
+ files = files()
183
+ return [osp.join(self.subgraph_dir, f) for f in list(files)]
184
+
185
+ def get_padding(self, log:bool, tile_df, full_df, padding:float):
186
+ r"""A helper function to get the padding area for a tile of tissue.
187
+ Args:
188
+ log (bool), whether to print logs
189
+ tile_df: the data.frame with current tile.
190
+ full_df: original tissue data.frame
191
+ padding: size of the padding (used for padding the core area of the tile)
192
+ """
193
+ left_most = tile_df[self.tile_by_dim].min()
194
+ right_most = tile_df[self.tile_by_dim].max()
195
+
196
+ padding_left_mask = (full_df[self.tile_by_dim] >= (left_most - padding)) & (full_df[self.tile_by_dim] < left_most)
197
+ padding_right_mask = (full_df[self.tile_by_dim] > (right_most)) & (full_df[self.tile_by_dim] <= (right_most + padding))
198
+
199
+ if log:
200
+ print("left padding shape ",padding_left_mask.sum())
201
+ print("right padding shape ",padding_right_mask.sum())
202
+
203
+ padding_left_df = full_df[padding_left_mask]
204
+ padding_right_df = full_df[padding_right_mask]
205
+
206
+ padding_left_df = padding_left_df.assign(core_mask = False)
207
+ padding_right_df = padding_right_df.assign(core_mask = False)
208
+
209
+ return pd.concat([padding_left_df,padding_right_df])
210
+
211
+
212
+ def generate_subgraph(self,**kwargs):
213
+ import random
214
+ import sys
215
+ if self.log:
216
+ print("To generate subgraphs for ",self.subgraph_paths)
217
+
218
+ if files_exist(self.subgraph_paths) and not self.force_resample:
219
+ print("Subgraphs already exist, skipping subgraph generation")
220
+ return
221
+ elif not files_exist(self.processed_paths):
222
+ sys.exit("Wrong logic in generate_subgraph function. Attempted to generate subgraphs when tile graphs are not available")
223
+
224
+ for s_id in self.process_tile_ids:
225
+ random.seed(self.seed)
226
+ if self.log:
227
+ print("Loading processed data tile for subgraphing ", osp.join(self.processed_dir,f'{self.sample_name}_data_tile{s_id}.pt'))
228
+
229
+ tile_data = torch.load(osp.join(self.processed_dir,f'{self.sample_name}_data_tile{s_id}.pt'),weights_only=True)
230
+ print("load tile data from ", osp.join(self.processed_dir,f'{self.sample_name}_data_tile{s_id}.pt'))
231
+ tile_data = Data(**tile_data[0])
232
+ print(tile_data)
233
+
234
+ if self.subgraph_mode == "link_based":
235
+ if self.log:
236
+ print("Generating subgraphs for tile ", s_id, "Sampling " ,self.num_sampled_edges, " edges")
237
+ sampled_edge_index = tile_data.edge_index[:,random.sample(range(tile_data.edge_index.shape[1]),self.num_sampled_edges)]
238
+
239
+ subgraph_loader = LinkNeighborLoader(
240
+ data = tile_data,
241
+ edge_label_index = sampled_edge_index,
242
+ **kwargs
243
+ )
244
+ # subgraph_g = []
245
+ # ## Get one batch as the sampled subgraph from the tissue tile
246
+ # for batch in subgraph_loader:
247
+ # subgraph_g = subgraph_g + [batch]
248
+ # break
249
+ elif self.subgraph_mode == "node_based":
250
+ if self.log:
251
+ print("Generating subgraphs for tile ", s_id, "Sampling ", self.num_seed_nodes, " nodes with ", self.num_walks, " random walks.")
252
+ random.seed(self.seed)
253
+ sample_nodes = random.sample(range(tile_data.x.squeeze().size()[0]),self.num_seed_nodes)
254
+ sample_nodes_bywalks = torch.tensor(sample_nodes * self.num_walks)
255
+ sample_nodes_bywalks = sample_nodes_bywalks.sort().values
256
+ from torch_cluster import random_walk
257
+ sampled_edges_by_random_walk = random_walk(
258
+ row = tile_data.edge_index[0],
259
+ col = tile_data.edge_index[1],
260
+ start = sample_nodes_bywalks,
261
+ walk_length = 1, # 1 to get immediate neighbours
262
+ return_edge_indices=False
263
+ )
264
+
265
+ if self.log:
266
+ print("batch size ", sampled_edges_by_random_walk.squeeze().size()[0])
267
+ print("start node ", sample_nodes_bywalks, "size " , sample_nodes_bywalks.size())
268
+ print(" sampled_edges_by_random_walk ", sampled_edges_by_random_walk)
269
+
270
+ subgraph_loader = LinkNeighborLoader(
271
+ data = tile_data,
272
+ batch_size = sampled_edges_by_random_walk.squeeze().size()[0],
273
+ edge_label_index = torch.stack(
274
+ [sampled_edges_by_random_walk[:,0],sampled_edges_by_random_walk[:,1]]
275
+ ),
276
+ **kwargs
277
+ )
278
+ subgraph_g = []
279
+ ## Get one batch as the sampled subgraph from the tissue tile
280
+ for batch in subgraph_loader:
281
+ subgraph_g = subgraph_g + [batch]
282
+ break
283
+ ## simplify g before saving
284
+ subgraph_g = Data(x = subgraph_g[0].x, edge_index = subgraph_g[0].edge_index, edge_label_index = subgraph_g[0].edge_label_index)
285
+ self.save([subgraph_g], osp.join(self.subgraph_dir,f'{self.sample_name}_subgraph_data_tile{s_id}.pt'))
286
+ def process(self)-> None:
287
+ # Load raw data
288
+ import numpy as np
289
+ if self.log:
290
+ print("Processing raw file ",self.raw_paths[0])
291
+ print("To create processed files ", self.processed_paths)
292
+
293
+ raw_data = pd.read_csv(self.raw_paths[0])
294
+ if self.log:
295
+ print(f'Raw data shape {raw_data.shape[0]}')
296
+
297
+ raw_data["trans_id"] = np.arange(raw_data.shape[0])
298
+ ## cutting along one selected axis
299
+ raw_data["tiles_range"] = pd.cut(
300
+ raw_data[self.tile_by_dim],
301
+ bins = self.num_tiles,
302
+ labels= ["tile"+str(x) for x in range(self.num_tiles)]
303
+ )
304
+
305
+ for tile in self.process_tile_ids:
306
+ tile_df = raw_data[raw_data.tiles_range == ("tile"+str(tile))]
307
+ if self.log:
308
+ print("tile core area shape ", tile_df.shape)
309
+ assert tile_df.shape[0] > 0, "The requested tile contains no transcripts, likely due to the tile id is not in the range 0:(num_tiles-1)"
310
+ tile_df = tile_df.assign(core_mask=True)
311
+ padding_df = self.get_padding(
312
+ log = self.log,
313
+ tile_df = tile_df,
314
+ full_df = raw_data,
315
+ padding = self.pad_hops * self.radius_r
316
+ )
317
+ tile_df = pd.concat([tile_df,padding_df])
318
+ if self.log:
319
+ print("core area plus paddings, shape ", tile_df.shape)
320
+
321
+ if isinstance(next(iter(self.one_hot_encoding.items()))[1],int):
322
+ feature_x = torch.tensor([self.one_hot_encoding[x] for x in tile_df[self.feature_col]])
323
+ else:
324
+ feature_x = torch.stack([self.one_hot_encoding[x] for x in tile_df[self.feature_col]])
325
+ xy_pos = torch.tensor(np.array(tile_df[[ self.dim_x , self.dim_y]]))
326
+
327
+ edge_index = radius_graph(xy_pos, r = self.radius_r, max_num_neighbors = self.max_num_neighbors)
328
+
329
+ data_1 = Data(
330
+ x = feature_x,
331
+ edge_index = edge_index,
332
+ trans_id = torch.tensor(list(tile_df.trans_id)),
333
+ core_mask = torch.tensor(list(tile_df.core_mask)))
334
+
335
+ self.save([data_1], osp.join(self.processed_dir,f'{self.sample_name}_data_tile{tile}.pt'))
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.2
2
+ Name: spatialrna
3
+ Version: 0.1.2
4
+ Summary: A package for spatial RNA graph data processing and analysis.
5
+ Author-email: Ruqian Lyu <rlyu@svi.edu.au>
6
+ License: MIT
7
+ Project-URL: repository, https://gitlab.svi.edu.au/biocellgen-private/spatialrna
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: torch
14
+ Requires-Dist: torch_geometric
15
+ Requires-Dist: pandas
16
+ Requires-Dist: numpy
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=6.0; extra == "dev"
19
+ Requires-Dist: twine>=4.0.2; extra == "dev"
20
+ Requires-Dist: build; extra == "dev"
21
+
22
+ ## A package that provides easier inferface to GNN models for single-molecule ST data
23
+
24
+
25
+
26
+ ## Pre-requisite packages installation
27
+
28
+ ```
29
+ ## Using Conda and pip
30
+
31
+ conda create -n dev-spatialrna
32
+ conda activate dev-spatialrna
33
+ conda install python=3.10.0
34
+
35
+ ## torch 2.4.0 cuda version 12.4
36
+ pip install torch==2.4.0 --index-url https://download.pytorch.org/whl/cu124
37
+
38
+ ## torch_geometric=2.6.1 https://pytorch-geometric.readthedocs.io/en/stable/install/installation.html#installation-via-pypi
39
+
40
+ pip install torch_geometric==2.6.1
41
+
42
+ ## Additional useful libraries https://pytorch-geometric.readthedocs.io/en/stable/install/installation.html#additional-libraries
43
+
44
+ pip install pyg_lib torch_scatter torch_sparse torch_cluster torch_spline_conv -f https://data.pyg.org/whl/torch-2.4.0+cu124.html
45
+
46
+ ```
47
+
48
+ ## Install spatialrna
49
+
50
+ ```
51
+ pip install spatialrna
52
+ ```
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ spatialrna/__init__.py
4
+ spatialrna/spatialrna.py
5
+ spatialrna.egg-info/PKG-INFO
6
+ spatialrna.egg-info/SOURCES.txt
7
+ spatialrna.egg-info/dependency_links.txt
8
+ spatialrna.egg-info/requires.txt
9
+ spatialrna.egg-info/top_level.txt
10
+ tests/test_data.py
11
+ tests/test_reload.py
12
+ tests/test_subgraph.py
13
+ tests/test_subgraph_mode.py
@@ -0,0 +1,9 @@
1
+ torch
2
+ torch_geometric
3
+ pandas
4
+ numpy
5
+
6
+ [dev]
7
+ pytest>=6.0
8
+ twine>=4.0.2
9
+ build
@@ -0,0 +1 @@
1
+ spatialrna
@@ -0,0 +1,225 @@
1
+ import os
2
+ import pytest
3
+ from spatialrna.spatialrna import SpatialRNA
4
+ import pandas as pd
5
+ from shutil import rmtree
6
+
7
+ def create_test_data():
8
+ # Create a sample dataset for testing
9
+ data = {
10
+ "gene": ["gene1", "gene1", "gene2", "gene3", "gene3", "gene3"],
11
+ "x": [1, 1, 2, 2, 2.5,8],
12
+ "y": [1, 2.0, 1, 1, 2.5,3],
13
+ }
14
+ df = pd.DataFrame(data)
15
+ os.makedirs("data/test_sample/raw", exist_ok=True)
16
+ df.to_csv("data/test_sample/raw/test_sample.csv", index=False)
17
+ return None
18
+
19
+ def clean_test_data():
20
+ if os.path.exists("data/test_sample/"):
21
+ rmtree("data/test_sample/")
22
+ clean_test_data()
23
+
24
+ def test_initialization():
25
+ create_test_data()
26
+ dataset = SpatialRNA(
27
+ root="data/test_sample/",
28
+ sample_name="test_sample",
29
+ radius_r=1.0,
30
+ tile_by_dim="y",
31
+ dim_x="x",
32
+ dim_y="y",
33
+ num_tiles=2,
34
+ load_type="tile",
35
+ process_tile_ids=[0],
36
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
37
+ log=False
38
+ )
39
+ assert dataset.num_tiles == 2
40
+ assert dataset.radius_r == 1.0
41
+ assert dataset.tile_by_dim == "y"
42
+ assert dataset.process_tile_ids == [0]
43
+ assert dataset.load_type == "tile"
44
+ assert dataset.process_mode == "tile"
45
+ assert dataset.load_tile_id == 0
46
+
47
+ clean_test_data()
48
+
49
+ def test_load_blank():
50
+ create_test_data()
51
+ dataset = SpatialRNA(
52
+ root="data/test_sample/",
53
+ sample_name="test_sample",
54
+ radius_r=1.0,
55
+ tile_by_dim="y",
56
+ dim_x="x",
57
+ dim_y="y",
58
+ num_tiles=2,
59
+ load_type="blank",
60
+ process_tile_ids=[0],
61
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
62
+ log=False
63
+ )
64
+ clean_test_data()
65
+ assert dataset[0] is None
66
+
67
+ def test_load_when_not_process():
68
+ create_test_data()
69
+ dataset2 = SpatialRNA(
70
+ root="data/test_sample/",
71
+ sample_name="test_sample",
72
+ process_mode="tile",
73
+ radius_r=1.0,
74
+ tile_by_dim="y",
75
+ dim_x="x",
76
+ dim_y="y",
77
+ num_tiles=2,
78
+ load_type="tile",
79
+ load_tile_id=1,
80
+ process_tile_ids=[0],
81
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
82
+ log=False
83
+ )
84
+ assert dataset2[0] is None
85
+ clean_test_data()
86
+
87
+ def test_new_process_id():
88
+ create_test_data()
89
+ dataset1 = SpatialRNA(
90
+ root="data/test_sample/",
91
+ sample_name="test_sample",
92
+ radius_r=1.0,
93
+ tile_by_dim="y",
94
+ dim_x="x",
95
+ dim_y="y",
96
+ num_tiles=2,
97
+ load_type="blank",
98
+ process_mode="tile",
99
+ process_tile_ids=[0],
100
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
101
+ log=False
102
+ )
103
+ ## tile 0 was processed but tile 1 has not.
104
+ print(os.listdir("data/test_sample/processed"))
105
+ assert not os.path.exists("data/test_sample/processed/test_sample_data_tile1.pt")
106
+ assert os.path.exists("data/test_sample/processed/test_sample_data_tile0.pt")
107
+ dataset2 = SpatialRNA(
108
+ root="data/test_sample/",
109
+ sample_name="test_sample",
110
+ radius_r=1.0,
111
+ tile_by_dim="y",
112
+ dim_x="x",
113
+ dim_y="y",
114
+ num_tiles=2,
115
+ load_type="tile",
116
+ process_mode="tile",
117
+ process_tile_ids=[1],
118
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
119
+ log=False
120
+ )
121
+ assert os.path.exists("data/test_sample/processed/test_sample_data_tile1.pt")
122
+
123
+ clean_test_data()
124
+
125
+ def test_tile_by_dim_y():
126
+ # Test the tile_by_dim, x or y method
127
+ create_test_data()
128
+
129
+ dataset = SpatialRNA(
130
+ root="data/test_sample/",
131
+ sample_name="test_sample",
132
+ radius_r=1.0,
133
+ tile_by_dim="y",
134
+ dim_x="x",
135
+ dim_y="y",
136
+ num_tiles=2,
137
+ load_type="tile",
138
+ load_tile_id=0,
139
+ pad_hops = 0,
140
+ process_tile_ids=[0],
141
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
142
+ log=False
143
+ )
144
+ assert dataset.tile_by_dim == "y"
145
+ assert dataset.radius_r == 1.0
146
+ assert dataset[0].x.size()[0] == 4
147
+ clean_test_data()
148
+
149
+
150
+ def test_tile_by_dim_x():
151
+ # Test the tile_by_dim, x or y method
152
+ create_test_data()
153
+
154
+ dataset = SpatialRNA(
155
+ root="data/test_sample/",
156
+ sample_name="test_sample",
157
+ radius_r=1.0,
158
+ tile_by_dim="x",
159
+ dim_x="x",
160
+ dim_y="y",
161
+ num_tiles=2,
162
+ load_type="tile",
163
+ load_tile_id=0,
164
+ process_tile_ids=[0],
165
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
166
+ log=False
167
+ )
168
+ assert dataset.tile_by_dim == "x"
169
+ assert dataset.radius_r == 1.0
170
+ assert dataset[0].x.size()[0] == 5
171
+ clean_test_data()
172
+
173
+
174
+ ## one tile = no tiling
175
+ def test_one_tile():
176
+ create_test_data()
177
+ dataset = SpatialRNA(
178
+ root="data/test_sample/",
179
+ sample_name="test_sample",
180
+ radius_r=1.0,
181
+ tile_by_dim="y",
182
+ dim_x="x",
183
+ dim_y="y",
184
+ num_tiles=1,
185
+ load_type="tile",
186
+ process_tile_ids=[0],
187
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
188
+ log=False
189
+ )
190
+ print(dataset[0].x)
191
+ print(dataset[0].edge_index.shape)
192
+ print(dataset[0].edge_index[0])
193
+ print(dataset[0].edge_index[1])
194
+ assert dataset[0].x.size()[0] == 6
195
+ assert dataset[0].edge_index.size()[0] == 2
196
+ assert dataset[0].edge_index.size()[1] == 2
197
+ clean_test_data()
198
+
199
+
200
+ ## one tile = no tiling ie. entire tissue region as one tile.
201
+ ## radius_r is non-inclusive
202
+ def test_one_tile_r11():
203
+ create_test_data()
204
+ dataset = SpatialRNA(
205
+ root="data/test_sample/",
206
+ sample_name="test_sample",
207
+ radius_r=1.1,
208
+ tile_by_dim="y",
209
+ dim_x="x",
210
+ dim_y="y",
211
+ num_tiles=1,
212
+ load_type="tile",
213
+ process_tile_ids=[0],
214
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
215
+ log=False
216
+ )
217
+ print(dataset[0].x)
218
+ print(dataset[0].edge_index.size)
219
+
220
+ print(dataset[0].edge_index[0])
221
+ print(dataset[0].edge_index[1])
222
+ assert dataset[0].x.size()[0] == 6
223
+ assert dataset[0].edge_index.size()[1] == 8
224
+ assert dataset[0].edge_index.size()[0] == 2
225
+ clean_test_data()
@@ -0,0 +1,137 @@
1
+ import os
2
+ import pytest
3
+ from spatialrna.spatialrna import SpatialRNA
4
+ import pandas as pd
5
+ from shutil import rmtree
6
+ from torch_geometric.sampler import NegativeSampling
7
+
8
+ def create_test_data():
9
+ # Create a sample dataset for testing
10
+ data = {
11
+ "gene": ["gene1", "gene1", "gene2", "gene3", "gene3", "gene3"],
12
+ "x": [1, 1, 2, 2, 2.5,8],
13
+ "y": [1, 2.0, 1, 1, 2.5,3],
14
+ }
15
+ df = pd.DataFrame(data)
16
+ os.makedirs("data/test_sample/raw", exist_ok=True)
17
+ df.to_csv("data/test_sample/raw/test_sample.csv", index=False)
18
+ return None
19
+
20
+ def clean_test_data():
21
+ if os.path.exists("data/test_sample/"):
22
+ rmtree("data/test_sample/")
23
+ clean_test_data()
24
+
25
+ def test_force_reload():
26
+ clean_test_data()
27
+ create_test_data()
28
+ # process mode with subgraph, tile graph will be created by init (after checking force_reload, etc)
29
+ # try:
30
+ dataset = SpatialRNA(
31
+ root="data/test_sample/",
32
+ sample_name="test_sample",
33
+ radius_r=1.1,
34
+ tile_by_dim="y",
35
+ dim_x="x",
36
+ dim_y="y",
37
+ num_tiles=2,
38
+ load_type="blank",
39
+ load_tile_id=0,
40
+ process_tile_ids=[0,1],
41
+ process_mode="subgraph",
42
+ subgraph_mode="link_based",
43
+ num_sampled_edges=2,
44
+ num_neighbors = [-1,-1],
45
+ batch_size = 4,
46
+ neg_sampling = NegativeSampling(mode="triplet",amount=1),
47
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
48
+ log=True
49
+ )
50
+ time_stamp1 = os.path.getmtime("data/test_sample/processed/test_sample_data_tile1.pt")
51
+ import time
52
+ time.sleep(3)
53
+ # force_reload controls only the re-processing of data_tile.pt
54
+ SpatialRNA(
55
+ root="data/test_sample/",
56
+ sample_name="test_sample",
57
+ radius_r=1.1,
58
+ tile_by_dim="y",
59
+ dim_x="x",
60
+ dim_y="y",
61
+ num_tiles=2,
62
+ force_reload=True,
63
+ load_type="blank",
64
+ load_tile_id=0,
65
+ process_tile_ids=[0,1],
66
+ process_mode="subgraph",
67
+ subgraph_mode="link_based",
68
+ num_sampled_edges=2,
69
+ num_neighbors = [-1,-1],
70
+ batch_size = 4,
71
+ neg_sampling = NegativeSampling(mode="triplet",amount=1),
72
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
73
+ log=True
74
+ )
75
+ time_stamp2 = os.path.getmtime("data/test_sample/processed/test_sample_data_tile1.pt")
76
+ assert time_stamp1 != time_stamp2
77
+
78
+
79
+ def test_force_resample():
80
+ clean_test_data()
81
+ create_test_data()
82
+ # process mode with subgraph, tile graph will be created by init (after checking force_reload, etc)
83
+ # try:
84
+ dataset = SpatialRNA(
85
+ root="data/test_sample/",
86
+ sample_name="test_sample",
87
+ radius_r=1.1,
88
+ tile_by_dim="y",
89
+ dim_x="x",
90
+ dim_y="y",
91
+ num_tiles=2,
92
+ load_type="blank",
93
+ load_tile_id=0,
94
+ process_tile_ids=[0,1],
95
+ process_mode="subgraph",
96
+ subgraph_mode="link_based",
97
+ num_sampled_edges=2,
98
+ num_neighbors = [-1,-1],
99
+ batch_size = 4,
100
+ neg_sampling = NegativeSampling(mode="triplet",amount=1),
101
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
102
+ log=True
103
+ )
104
+ time_stamp1_tile1 = os.path.getmtime("data/test_sample/processed/test_sample_data_tile1.pt")
105
+ time_stamp1_subgraph_tile1 = os.path.getmtime("data/test_sample/subgraph/test_sample_subgraph_data_tile1.pt")
106
+
107
+ import time
108
+ time.sleep(3)
109
+
110
+ # force_resample controls only the re-sampling of subgraphs from data_tile.pt
111
+ SpatialRNA(
112
+ root="data/test_sample/",
113
+ sample_name="test_sample",
114
+ radius_r=1.1,
115
+ tile_by_dim="y",
116
+ dim_x="x",
117
+ dim_y="y",
118
+ num_tiles=2,
119
+ force_reload=False,
120
+ force_resample=True,
121
+ load_type="blank",
122
+ load_tile_id=0,
123
+ process_tile_ids=[0,1],
124
+ process_mode="subgraph",
125
+ subgraph_mode="link_based",
126
+ num_sampled_edges=2,
127
+ num_neighbors = [-1,-1],
128
+ batch_size = 4,
129
+ neg_sampling = NegativeSampling(mode="triplet",amount=1),
130
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
131
+ log=True
132
+ )
133
+ time_stamp2_tile1 = os.path.getmtime("data/test_sample/processed/test_sample_data_tile1.pt")
134
+ time_stamp2_subgraph_tile1 = os.path.getmtime("data/test_sample/subgraph/test_sample_subgraph_data_tile1.pt")
135
+
136
+ assert time_stamp1_tile1 == time_stamp2_tile1
137
+ assert time_stamp1_subgraph_tile1 != time_stamp2_subgraph_tile1
@@ -0,0 +1,98 @@
1
+ import os
2
+ import pytest
3
+ from spatialrna.spatialrna import SpatialRNA
4
+ import pandas as pd
5
+ from shutil import rmtree
6
+ from torch_geometric.sampler import NegativeSampling
7
+
8
+ def create_test_data():
9
+ # Create a sample dataset for testing
10
+ data = {
11
+ "gene": ["gene1", "gene1", "gene2", "gene3", "gene3", "gene3"],
12
+ "x": [1, 1, 2, 2, 2.5,8],
13
+ "y": [1, 2.0, 1, 1, 2.5,3],
14
+ }
15
+ df = pd.DataFrame(data)
16
+ os.makedirs("data/test_sample/raw", exist_ok=True)
17
+ df.to_csv("data/test_sample/raw/test_sample.csv", index=False)
18
+ return None
19
+
20
+ def clean_test_data():
21
+ if os.path.exists("data/test_sample/"):
22
+ rmtree("data/test_sample/")
23
+ clean_test_data()
24
+
25
+ def test_process_order_subgraph():
26
+ clean_test_data()
27
+ create_test_data()
28
+ # process mode with subgraph, tile graph will be created by init (after checking force_reload, etc)
29
+ # try:
30
+ dataset = SpatialRNA(
31
+ root="data/test_sample/",
32
+ sample_name="test_sample",
33
+ radius_r=15,
34
+ tile_by_dim="y",
35
+ dim_x="x",
36
+ dim_y="y",
37
+ num_tiles=2,
38
+ load_type="subgraph",
39
+ load_tile_id=0,
40
+ process_tile_ids=[1],
41
+ process_mode="subgraph",
42
+ subgraph_mode="link_based",
43
+ num_sampled_edges=2,
44
+ num_neighbors = [-1,-1],
45
+ batch_size = 4,
46
+ neg_sampling = NegativeSampling(mode="triplet",amount=1),
47
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
48
+ log=True
49
+ )
50
+ assert dataset[0] is None
51
+ # except Exception as e:
52
+ # assert isinstance(e, TypeError)
53
+ dataset = SpatialRNA(
54
+ root="data/test_sample/",
55
+ sample_name="test_sample",
56
+ radius_r=11,
57
+ tile_by_dim="y",
58
+ dim_x="x",
59
+ dim_y="y",
60
+ num_tiles=2,
61
+ load_type="subgraph",
62
+ load_tile_id=1,
63
+ process_tile_ids=[0],
64
+ process_mode="subgraph",
65
+ subgraph_mode="link_based",
66
+ num_sampled_edges=2,
67
+ num_neighbors = [-1,-1],
68
+ batch_size = 4,
69
+ neg_sampling = NegativeSampling(mode="triplet",amount=1),
70
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
71
+ log=True
72
+ )
73
+ clean_test_data()
74
+ create_test_data()
75
+ dataset = SpatialRNA(
76
+ root="data/test_sample/",
77
+ sample_name="test_sample",
78
+ radius_r=11,
79
+ tile_by_dim="y",
80
+ dim_x="x",
81
+ dim_y="y",
82
+ num_tiles=2,
83
+ load_type="subgraph",
84
+ load_tile_id=0,
85
+ process_tile_ids=[0],
86
+ process_mode="subgraph",
87
+ subgraph_mode="link_based",
88
+ num_sampled_edges=2,
89
+ num_neighbors = [-1,-1],
90
+ batch_size = 4,
91
+ neg_sampling = NegativeSampling(mode="triplet",amount=1),
92
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
93
+ log=False
94
+ )
95
+
96
+ #clean_test_data()
97
+
98
+
@@ -0,0 +1,136 @@
1
+ import os
2
+ import pytest
3
+ from spatialrna.spatialrna import SpatialRNA
4
+ import pandas as pd
5
+ from shutil import rmtree
6
+ from torch_geometric.sampler import NegativeSampling
7
+
8
+ def create_test_data():
9
+ # Create a sample dataset for testing
10
+ data = {
11
+ "gene": ["gene1", "gene1", "gene2", "gene3", "gene3", "gene3"],
12
+ "x": [1, 1, 2, 2, 2.5,8],
13
+ "y": [1, 2.0, 1, 1, 2.5,3],
14
+ }
15
+ df = pd.DataFrame(data)
16
+ os.makedirs("data/test_sample/raw", exist_ok=True)
17
+ df.to_csv("data/test_sample/raw/test_sample.csv", index=False)
18
+ return None
19
+
20
+ def clean_test_data():
21
+ if os.path.exists("data/test_sample/"):
22
+ rmtree("data/test_sample/")
23
+
24
+ def test_subgraph_mode_basic():
25
+ create_test_data()
26
+ SpatialRNA(
27
+ root="data/test_sample/",
28
+ sample_name="test_sample",
29
+ radius_r=1.1,
30
+ tile_by_dim="y",
31
+ num_tiles=2,
32
+ dim_x="x",
33
+ dim_y="y",
34
+ load_type="blank",
35
+ pad_hops=0,
36
+ process_tile_ids=[0,1],
37
+ process_mode="tile",
38
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
39
+ log=False
40
+ )
41
+ assert os.path.exists("data/test_sample/processed/test_sample_data_tile0.pt")
42
+ assert os.path.exists("data/test_sample/processed/test_sample_data_tile1.pt")
43
+ # process mode with subgraph, when tile was not created, the tile graph will be created.
44
+ clean_test_data()
45
+ create_test_data()
46
+ subgraphs = SpatialRNA(
47
+ root="data/test_sample/",
48
+ sample_name="test_sample",
49
+ radius_r=11,
50
+ tile_by_dim="y",
51
+ num_tiles=2,
52
+ dim_x="x",
53
+ dim_y="y",
54
+ load_tile_id=1,
55
+ load_type="subgraph",
56
+ pad_hops=0,
57
+ process_tile_ids=[1],
58
+ process_mode="subgraph",
59
+ subgraph_mode="link_based",
60
+ num_sampled_edges=2,
61
+ num_neighbors = [-1,-1],
62
+ batch_size = 2,
63
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
64
+ log=False
65
+ )
66
+ assert not os.path.exists("data/test_sample/subgraph/test_sample_subgraph_data_tile0.pt")
67
+ assert os.path.exists("data/test_sample/subgraph/test_sample_subgraph_data_tile1.pt")
68
+
69
+ subgraphs = SpatialRNA(
70
+ root="data/test_sample/",
71
+ sample_name="test_sample",
72
+ radius_r=1.1,
73
+ tile_by_dim="y",
74
+ dim_x="x",
75
+ dim_y="y",
76
+ num_tiles=2,
77
+ load_type="subgraph",
78
+ load_tile_id=1,
79
+ pad_hops=0,
80
+ process_tile_ids=[0],
81
+ process_mode="subgraph",
82
+ subgraph_mode="node_based",
83
+ force_resample=True,
84
+ num_seed_nodes=2,
85
+ num_neighbors = [-1,-1],
86
+ num_walks=2,
87
+ # batch_size = 2, batch size is inferred when subgraph mode is node_based with random walks
88
+ one_hot_encoding={"gene1": 0, "gene2": 1, "gene3": 3},
89
+ log=False
90
+ )
91
+ assert os.path.exists("data/test_sample/subgraph/test_sample_subgraph_data_tile0.pt")
92
+ assert os.path.exists("data/test_sample/subgraph/test_sample_subgraph_data_tile1.pt")
93
+
94
+ clean_test_data()
95
+
96
+
97
+ def test_subgraph_node_based():
98
+ import shutil
99
+
100
+ if os.path.exists("data/test_df/subgraph"):
101
+ shutil.rmtree("data/test_df/subgraph")
102
+ if os.path.exists("data/test_df/processed"):
103
+ shutil.rmtree("data/test_df/processed")
104
+ # the loaded subgraph data object is a list of 1
105
+ subgraphs = SpatialRNA(
106
+ root="data/test_df/",
107
+ sample_name="test_df",
108
+ radius_r=15,
109
+ tile_by_dim="y",
110
+ dim_x="x",
111
+ dim_y="y",
112
+ num_tiles=2,
113
+ load_type="subgraph",
114
+ load_tile_id=1,
115
+ pad_hops=0,
116
+ process_tile_ids=[0,1],
117
+ process_mode="subgraph",
118
+ subgraph_mode="node_based",
119
+ force_resample=True,
120
+ num_seed_nodes=4,
121
+ num_neighbors = [-1,-1],
122
+ subgraph_type= "bidirectional",
123
+ num_walks=2,
124
+ # batch_size = 2, batch size is inferred when subgraph mode is node_based with random walks
125
+ one_hot_encoding={"A": 0, "B": 1, "C": 3},
126
+ seed = 100,
127
+ log=True
128
+ )
129
+ import torch
130
+ assert subgraphs[0].x.size()[0] == 8
131
+ #[3, 3, 0, 0, 1, 3, 3, 0]
132
+ assert subgraphs[0].x[0] == 3
133
+ assert subgraphs[0].x[1] == 3
134
+ assert subgraphs[0].x[2] == 0
135
+
136
+ assert (subgraphs[0].edge_label_index.size()[1] == 8)