spatialrna 0.1.2__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.
spatialrna/__init__.py ADDED
@@ -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,6 @@
1
+ spatialrna/__init__.py,sha256=41sOzuhNv4B80hWXhIynbGDmfJWWHV_mpxlZl4ZiErA,35
2
+ spatialrna/spatialrna.py,sha256=apVvLAorOy45B2dpEnUBWj7m_cZL2M1doU6Csxee0ew,15572
3
+ spatialrna-0.1.2.dist-info/METADATA,sha256=_Q18EJvEOz52XxNxGX4w8CFwPQAz-Qfa9e6viTqTx5Y,1553
4
+ spatialrna-0.1.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
+ spatialrna-0.1.2.dist-info/top_level.txt,sha256=ut3eWOeVxuENQDeJnQpkMdZWj05UCaHadGGT1tdX3vU,11
6
+ spatialrna-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ spatialrna