spatialrna 0.1.2__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ruqian Lyu
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.
@@ -1,15 +1,16 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: spatialrna
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: A package for spatial RNA graph data processing and analysis.
5
5
  Author-email: Ruqian Lyu <rlyu@svi.edu.au>
6
6
  License: MIT
7
- Project-URL: repository, https://gitlab.svi.edu.au/biocellgen-private/spatialrna
7
+ Project-URL: repository, https://github.com/ruqianl/spatialrna.git
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
+ License-File: LICENSE.txt
13
14
  Requires-Dist: torch
14
15
  Requires-Dist: torch_geometric
15
16
  Requires-Dist: pandas
@@ -18,6 +19,7 @@ Provides-Extra: dev
18
19
  Requires-Dist: pytest>=6.0; extra == "dev"
19
20
  Requires-Dist: twine>=4.0.2; extra == "dev"
20
21
  Requires-Dist: build; extra == "dev"
22
+ Dynamic: license-file
21
23
 
22
24
  ## A package that provides easier inferface to GNN models for single-molecule ST data
23
25
 
@@ -43,6 +45,14 @@ pip install torch_geometric==2.6.1
43
45
 
44
46
  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
47
 
48
+ ## clustering and plotting requires
49
+
50
+ pip install tqdm \
51
+ scipy \
52
+ scikit-learn \
53
+ pandas \
54
+ matplotlib \
55
+ opencv-python
46
56
  ```
47
57
 
48
58
  ## Install spatialrna
@@ -50,3 +60,7 @@ pip install pyg_lib torch_scatter torch_sparse torch_cluster torch_spline_conv -
50
60
  ```
51
61
  pip install spatialrna
52
62
  ```
63
+
64
+ ## Documentations
65
+
66
+ https://ruqianl.github.io/spatialrna_docs/
@@ -22,10 +22,22 @@ pip install torch_geometric==2.6.1
22
22
 
23
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
24
 
25
+ ## clustering and plotting requires
26
+
27
+ pip install tqdm \
28
+ scipy \
29
+ scikit-learn \
30
+ pandas \
31
+ matplotlib \
32
+ opencv-python
25
33
  ```
26
34
 
27
35
  ## Install spatialrna
28
36
 
29
37
  ```
30
38
  pip install spatialrna
31
- ```
39
+ ```
40
+
41
+ ## Documentations
42
+
43
+ https://ruqianl.github.io/spatialrna_docs/
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spatialrna"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  description="A package for spatial RNA graph data processing and analysis."
9
9
  readme = {file = "README.md", content-type = "text/markdown"}
10
10
  authors = [
@@ -38,7 +38,7 @@ include = ["spatialrna"]
38
38
  # https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#automatic-discovery
39
39
 
40
40
  [project.urls]
41
- repository = "https://gitlab.svi.edu.au/biocellgen-private/spatialrna"
41
+ repository = "https://github.com/ruqianl/spatialrna.git"
42
42
 
43
43
  [project.optional-dependencies]
44
44
  dev = [
@@ -0,0 +1,113 @@
1
+ import os
2
+ import os.path as osp
3
+ import glob
4
+ import numpy as np
5
+ import pandas as pd
6
+ from sklearn.cluster import KMeans
7
+ import joblib
8
+
9
+ def run_kmeans(
10
+ sample_name_list=None,
11
+ root="../data/",
12
+ downsample_to=None,
13
+ downsample_seed=1024,
14
+ verbose=True,
15
+ split_file_per_sample=True,
16
+ **kwargs
17
+ ) -> None:
18
+ """
19
+ Cluster embeddings from multiple samples using KMeans.
20
+
21
+ Args:
22
+ sample_name_list (list[str], optional): List of sample names to cluster. Defaults to None (auto-detect).
23
+ root (str): Path to data directory containing samples.
24
+ downsample_to (int, optional): Maximum number of rows per sample. Defaults to None (no downsampling).
25
+ downsample_seed (int): Random seed for downsampling.
26
+ verbose (bool, optional): Print progress. Defaults to True.
27
+ split_file_per_sample (bool, optional): Save per-sample CSVs if True; else save one CSV.
28
+ **kwargs: Additional arguments for sklearn KMeans (e.g., n_clusters).
29
+
30
+ Returns:
31
+ pd.DataFrame: DataFrame with columns ['sample_name','tx_ids','cluster_labels']
32
+ """
33
+
34
+ if sample_name_list is None:
35
+ # Detect sample names with 'embedding' subdir
36
+ sample_name_list = [
37
+ d for d in os.listdir(root)
38
+ if osp.isdir(osp.join(root, d)) and osp.isdir(osp.join(root, d, "embedding"))
39
+ ]
40
+
41
+ if verbose:
42
+ print(f"Clustering samples: {sample_name_list}")
43
+
44
+ all_embeddings = []
45
+ all_tx_meta = []
46
+
47
+ np.random.seed(downsample_seed)
48
+
49
+ for sample_name in sample_name_list:
50
+ data_path = osp.join(root, sample_name, "embedding")
51
+
52
+ # Load embeddings
53
+ npy_list = glob.glob(osp.join(data_path, "*.npy"))
54
+ #print(npy_list)
55
+ if not npy_list:
56
+ raise FileNotFoundError(f"No .npy files found for sample {sample_name}")
57
+ x_list = [np.load(f) for f in npy_list]
58
+ x_sample = np.concatenate(x_list, axis=0)
59
+
60
+ # Load tx_id files, make sure the same order as npy files.
61
+ tx_id_list = [x.replace(".npy","input_tx_id.csv") for x in npy_list]
62
+ #print(tx_id_list)
63
+ if not tx_id_list:
64
+ raise FileNotFoundError(f"No *input_tx_id.csv files found for sample {sample_name}")
65
+ tx_ids_emb = pd.concat([pd.read_csv(f) for f in tx_id_list], axis=0)
66
+ tx_ids_emb['sample_name'] = sample_name
67
+
68
+ # Optional downsampling
69
+ if downsample_to is not None and tx_ids_emb.shape[0] > downsample_to:
70
+ tx_ids_emb = tx_ids_emb.sample(n=downsample_to, random_state=downsample_seed).reset_index(drop=True)
71
+ idx = np.random.choice(x_sample.shape[0], downsample_to, replace=False)
72
+ x_sample = x_sample[idx, :]
73
+
74
+ all_embeddings.append(x_sample)
75
+ all_tx_meta.append(tx_ids_emb)
76
+
77
+ if verbose:
78
+ print(f"{sample_name}: embeddings {x_sample.shape}, tx_ids {tx_ids_emb.shape}")
79
+
80
+ # Concatenate all samples
81
+ X_all = np.concatenate(all_embeddings, axis=0)
82
+ tx_meta_all = pd.concat(all_tx_meta, axis=0).reset_index(drop=True)
83
+
84
+ # KMeans parameters, default 10
85
+ n_clusters = kwargs.pop('n_clusters', 10)
86
+ km = KMeans(n_clusters=n_clusters, **kwargs)
87
+ km.fit(X_all)
88
+
89
+ # Build result DataFrame
90
+ res_all = pd.DataFrame({
91
+ 'sample_name': tx_meta_all['sample_name'],
92
+ 'tx_ids': tx_meta_all['tx_id'],
93
+ 'cluster_labels': km.labels_.astype(str)
94
+ })
95
+
96
+ # Save results
97
+ if split_file_per_sample:
98
+ for sample_name in tx_meta_all['sample_name'].unique():
99
+ out_dir = osp.join(root, sample_name, "clusters")
100
+ os.makedirs(out_dir, exist_ok=True)
101
+ res_all[res_all['sample_name'] == sample_name].to_csv(
102
+ osp.join(out_dir, f"{sample_name}.{n_clusters}clusters.csv"),
103
+ index=False
104
+ )
105
+ else:
106
+ out_file = osp.join(root, f'kmeans.{n_clusters}clusters.csv')
107
+ res_all.to_csv(out_file, index=False)
108
+
109
+ if verbose:
110
+ print("Clustering completed.")
111
+ joblib.dump(km, osp.join(root, f'kmeans.{n_clusters}clusters.models.joblib'))
112
+
113
+ None
@@ -12,7 +12,7 @@ from torch_geometric.loader import LinkNeighborLoader
12
12
 
13
13
  ## feature matrix. Reduce mem and I/O by mapping gene label to int IDs. Delay mapping to 1-D tensors when training.
14
14
  ## TODO: warnings about different num_tiles, radius_r arguments if only loading not processing
15
-
15
+
16
16
  def files_exist(files: List[str]) -> bool:
17
17
  # NOTE: We return `False` in case `files` is empty, leading to a
18
18
  # re-processing of files on every instantiation.
@@ -38,17 +38,9 @@ class SpatialRNA(InMemoryDataset):
38
38
  process_tile_ids (List[int]): Select tiles to process including generating tile graph and subgraph.
39
39
  one_hot_encoding (dict): Dictionary that mapps gene names to integer (or one-hot-encoding tensors).
40
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.
41
+ force_reload (bool, optional): Whether to re-generate and overwrite the existing tile data.
50
42
  (default: :obj:`False`)
51
- force_resample (bool, optional): Whether to re-process the tile dataset to generate subgraphs.
43
+ force_resample (bool, optional): Whether to re-generate and overwrite the existing subgraph data.
52
44
  (default: :obj:`False`)
53
45
  subgraph_mode (str): The mode of subgraph generation can be either "link_based" or "node_based".
54
46
  (default: :obj:`link_based`)
@@ -57,7 +49,9 @@ class SpatialRNA(InMemoryDataset):
57
49
  (default: :obj:`5000`)
58
50
  num_walks:(int, optional): Number of walks for generating subgraphs.
59
51
  (default: :obj:`5`)
60
-
52
+ pred_label_col: (str): The column name that stores the classification label i.e., cell type. (default: None)
53
+ pred_label_map: (dict): A label id to int id categories mapping dictionary. For example, map 10 cell types to 0 - 9 (default: None)
54
+ pred_label_cell_id_col: (str): The column name that stores the segmentation cell id. It is used for making transcript graphs isolated by segmentation boundaries. (default: None)
61
55
  log (bool, optional): Whether to print any console output while
62
56
  downloading and processing the dataset. (default: :obj:`True`)
63
57
  **kwargs (optional): Additional arguments of
@@ -88,6 +82,9 @@ class SpatialRNA(InMemoryDataset):
88
82
  pre_transform = None,
89
83
  force_reload: bool = False,
90
84
  force_resample: bool = False,
85
+ pred_label_col: str = None,
86
+ pred_label_map: dict = None,
87
+ pred_label_cell_id_col: str = None,
91
88
  log: bool= True,
92
89
  seed: int = 100,
93
90
  **kwargs,
@@ -115,9 +112,8 @@ class SpatialRNA(InMemoryDataset):
115
112
  self.process_mode = process_mode
116
113
  self.num_seed_nodes = num_seed_nodes
117
114
  self.num_walks = num_walks
118
-
119
115
  assert tile_by_dim in [dim_x,dim_y]
120
- assert one_hot_encoding is not None
116
+
121
117
 
122
118
  self.dim_x = dim_x
123
119
  self.dim_y = dim_y
@@ -130,16 +126,22 @@ class SpatialRNA(InMemoryDataset):
130
126
  self.process_tile_ids = [load_tile_id]
131
127
  else:
132
128
  self.process_tile_ids = process_tile_ids
129
+ self.pred_label_col = pred_label_col
130
+ if self.pred_label_col is not None:
131
+ assert pred_label_map is not None, "Running with prediction model, pred_label_map is required."
132
+ self.pred_label_map = pred_label_map
133
+ self.pred_label_cell_id_col = pred_label_cell_id_col
134
+
133
135
  # super init, process tiles, if tiles.pt not exist, and regenerate if force_reload=True.
134
136
  super().__init__(root, transform, pre_transform,force_reload=force_reload, log=log)
135
137
  # subsequently generates the subgraphs if requested
136
138
  if process_mode == "subgraph":
137
139
  self.generate_subgraph(**kwargs)
140
+ else:
141
+ assert one_hot_encoding is not None
138
142
 
139
143
  if load_type == 'subgraph':
140
144
  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
145
  elif load_type == 'tile':
144
146
  load_file_name = osp.join(self.processed_dir,f'{self.sample_name}_data_tile{self.load_tile_id}.pt')
145
147
  else:
@@ -153,8 +155,7 @@ class SpatialRNA(InMemoryDataset):
153
155
  else:
154
156
  print(load_file_name," not exist, nothing loaded")
155
157
  None
156
-
157
-
158
+
158
159
  @property
159
160
  def subgraph_dir(self) -> str:
160
161
  return osp.join(self.root,"subgraph")
@@ -227,10 +228,10 @@ class SpatialRNA(InMemoryDataset):
227
228
  print("Loading processed data tile for subgraphing ", osp.join(self.processed_dir,f'{self.sample_name}_data_tile{s_id}.pt'))
228
229
 
229
230
  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
+ if self.log:
232
+ print("load tile data from ", osp.join(self.processed_dir,f'{self.sample_name}_data_tile{s_id}.pt'))
231
233
  tile_data = Data(**tile_data[0])
232
234
  print(tile_data)
233
-
234
235
  if self.subgraph_mode == "link_based":
235
236
  if self.log:
236
237
  print("Generating subgraphs for tile ", s_id, "Sampling " ,self.num_sampled_edges, " edges")
@@ -241,11 +242,7 @@ class SpatialRNA(InMemoryDataset):
241
242
  edge_label_index = sampled_edge_index,
242
243
  **kwargs
243
244
  )
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
245
+
249
246
  elif self.subgraph_mode == "node_based":
250
247
  if self.log:
251
248
  print("Generating subgraphs for tile ", s_id, "Sampling ", self.num_seed_nodes, " nodes with ", self.num_walks, " random walks.")
@@ -265,7 +262,7 @@ class SpatialRNA(InMemoryDataset):
265
262
  if self.log:
266
263
  print("batch size ", sampled_edges_by_random_walk.squeeze().size()[0])
267
264
  print("start node ", sample_nodes_bywalks, "size " , sample_nodes_bywalks.size())
268
- print(" sampled_edges_by_random_walk ", sampled_edges_by_random_walk)
265
+ print("sampled_edges_by_random_walk ", sampled_edges_by_random_walk)
269
266
 
270
267
  subgraph_loader = LinkNeighborLoader(
271
268
  data = tile_data,
@@ -275,14 +272,15 @@ class SpatialRNA(InMemoryDataset):
275
272
  ),
276
273
  **kwargs
277
274
  )
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'))
275
+ ## When use supply NegativeSampling, there is no edge_label_index returned. but neg_src and neg_pos_index
276
+ # Get one batch as the sampled subgraph from the tissue tile
277
+ batch = next(iter(subgraph_loader))
278
+ self.save(
279
+ [batch],
280
+ osp.join(self.subgraph_dir, f"{self.sample_name}_subgraph_data_tile{s_id}.pt")
281
+ )
282
+
283
+
286
284
  def process(self)-> None:
287
285
  # Load raw data
288
286
  import numpy as np
@@ -317,19 +315,38 @@ class SpatialRNA(InMemoryDataset):
317
315
  tile_df = pd.concat([tile_df,padding_df])
318
316
  if self.log:
319
317
  print("core area plus paddings, shape ", tile_df.shape)
320
-
318
+ # Sort dataframe by the cell id column, which is used to form batch_id for radius_graph function
319
+ if self.pred_label_cell_id_col is not None:
320
+ tile_df = tile_df.sort_values(by=self.pred_label_cell_id_col).reset_index(drop=True)
321
321
  if isinstance(next(iter(self.one_hot_encoding.items()))[1],int):
322
322
  feature_x = torch.tensor([self.one_hot_encoding[x] for x in tile_df[self.feature_col]])
323
323
  else:
324
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
-
325
+ xy_pos = torch.tensor(
326
+ tile_df[[self.dim_x, self.dim_y]].values,
327
+ dtype=torch.float
328
+ )
329
+ if self.pred_label_cell_id_col is not None:
330
+ ## self.pred_label_cell_id_col needs to be sorted. [0,1,0,1] won't work
331
+ codes, uniques = pd.factorize(tile_df[self.pred_label_cell_id_col])
332
+ batch = torch.tensor(codes, dtype=torch.long)
333
+ edge_index = radius_graph(
334
+ xy_pos,
335
+ r=self.radius_r,
336
+ max_num_neighbors=self.max_num_neighbors,
337
+ batch=batch
338
+ )
339
+ else:
340
+ edge_index = radius_graph(
341
+ xy_pos,
342
+ r=self.radius_r,
343
+ max_num_neighbors=self.max_num_neighbors
344
+ )
329
345
  data_1 = Data(
330
346
  x = feature_x,
331
347
  edge_index = edge_index,
332
348
  trans_id = torch.tensor(list(tile_df.trans_id)),
333
349
  core_mask = torch.tensor(list(tile_df.core_mask)))
334
-
350
+ if self.pred_label_col is not None:
351
+ data_1.y = torch.tensor(tile_df[self.pred_label_col].map(self.pred_label_map).values)
335
352
  self.save([data_1], osp.join(self.processed_dir,f'{self.sample_name}_data_tile{tile}.pt'))
@@ -0,0 +1,74 @@
1
+ import os
2
+ import glob
3
+ import torch
4
+ from torch_geometric.data import Database, OnDiskDataset, Data
5
+
6
+ class SpatialRNAOnDiskDatasetSQL(OnDiskDataset):
7
+ def __init__(self, root, pt_dir="subgraph", use_sqlite=False,
8
+ transform=None, pre_transform=None):
9
+ self.pt_dir = pt_dir
10
+
11
+ # If SQLite backend is requested, point it to a database path
12
+ super().__init__(root, transform, pre_transform, backend="sqlite")
13
+
14
+ @property
15
+ def processed_file_names(self):
16
+ #os.path.join(root, "sqlite.db")
17
+ #files = glob.glob(os.path.join(self.root, "*", self.pt_dir, "*.pt"))
18
+ #return os.path.join(self.root, "sqlite.db")
19
+ return "sqlite.db"
20
+
21
+ def pt_file_names(self):
22
+ # All .pt files under root/**/pt_dir/
23
+ files = glob.glob(os.path.join(self.root, "*", self.pt_dir, "*.pt"))
24
+ return sorted(files)
25
+ # @property
26
+ # def processed_dir(self) -> str:
27
+ # return os.path.join(self.root, '/')
28
+
29
+ def process(self):
30
+ all_pt_files = self.pt_file_names()
31
+ print(self.processed_paths)
32
+ for pt_file in all_pt_files:
33
+ data = Data(**torch.load(pt_file)[0],weights_only=True)
34
+ self.append(self.serialize(data))
35
+
36
+ # def len(self):
37
+ # return len(self.processed_file_names)
38
+
39
+ # def get(self, idx):
40
+ # path = self.processed_file_names[idx]
41
+ # return torch.load(path)
42
+
43
+
44
+ class SpatialRNAOnDiskDataset(OnDiskDataset):
45
+ def __init__(self, root, pt_dir="subgraph", transform=None, pre_transform=None):
46
+ self._pt_dir = pt_dir # use a private var
47
+ super().__init__(root, transform, pre_transform)
48
+ None
49
+
50
+ @property
51
+ def processed_file_names(self):
52
+ # Find all .pt files under root/**/processed/
53
+ files = glob.glob(os.path.join(self.root, "*", self._pt_dir, "*_data_tile*.pt"))
54
+ return sorted(files)
55
+
56
+ def process(self):
57
+ # Already done – nothing to process.
58
+ pass
59
+
60
+ def len(self):
61
+ return len(self.processed_file_names)
62
+
63
+ def get(self, idx):
64
+ path = self.processed_file_names[idx]
65
+ data = torch.load(path,weights_only=True)
66
+ return Data(**data[0])
67
+ def multi_get(self, idx_list):
68
+ """Load multiple graphs at once, similar to SQLite-backed OnDiskDataset."""
69
+ data_list = []
70
+ for idx in idx_list:
71
+ path = self.processed_file_names[idx]
72
+ data = torch.load(path, weights_only=True)
73
+ data_list.append(Data(**data[0]))
74
+ return data_list
@@ -0,0 +1,387 @@
1
+ import torch
2
+ import torch.nn.functional as F
3
+ import numpy as np
4
+ import random as rn
5
+ from torch_geometric.loader import LinkNeighborLoader,NeighborLoader
6
+ from torch_geometric.sampler import NegativeSampling
7
+ from tqdm import tqdm
8
+ import os.path as osp
9
+ import os
10
+ import pandas as pd
11
+ from torch_geometric.data import Data
12
+
13
+ @torch.no_grad()
14
+ def validate(
15
+ model,
16
+ val_loader,
17
+ device,
18
+ num_classes: int,
19
+ batch_size: int = 2048,
20
+ num_neighbors: list[int] = [20, 10],
21
+ verbose: bool = True,
22
+ num_train_edges: int = 5000,
23
+ seed = 42
24
+ ) -> float:
25
+ """
26
+ Run validation for link prediction task.
27
+
28
+ Args:
29
+ model (torch.nn.Module): The GNN model to evaluate.
30
+ val_loader (torch.utils.data.DataLoader): Loader with validation graphs.
31
+ device (torch.device): Device to run evaluation on.
32
+ num_classes (int): Number of node feature classes (for one-hot encoding).
33
+ batch_size (int, optional): Batch size for LinkNeighborLoader. Defaults to 2048.
34
+ num_neighbors (list[int], optional): Neighbor sampling sizes. Defaults to [20, 10].
35
+ verbose (bool, optional): Whether to print progress. Defaults to True.
36
+
37
+ Returns:
38
+ float: Mean validation accuracy.
39
+ """
40
+ rn.seed(seed)
41
+ model.eval()
42
+ all_acc = []
43
+
44
+ for d_i, d_batch in enumerate(val_loader):
45
+ if verbose:
46
+ print(f"Processing validation on batch graph {d_i}...")
47
+ sampled_edge_index = getattr(d_batch, "edge_label_index", None)
48
+ if sampled_edge_index is None:
49
+ sampled_edge_index = d_batch.edge_index[:,rn.sample(range(d_batch.edge_index.shape[1]),num_train_edges)]
50
+ val_mini_loader = LinkNeighborLoader(
51
+ d_batch,
52
+ batch_size=batch_size,
53
+ shuffle=False,
54
+ edge_label_index=sampled_edge_index,
55
+ neg_sampling=NegativeSampling(mode="triplet", amount=1),
56
+ num_neighbors=num_neighbors,
57
+ disjoint=False,
58
+ subgraph_type="bidirectional",
59
+ )
60
+
61
+ batch_acc = []
62
+ for batch in tqdm(val_mini_loader, desc=f"Val minibatching {d_i}", disable=not verbose):
63
+ batch = batch.to(device)
64
+ # Build edge labels
65
+ pos_edges = torch.stack([batch.src_index, batch.dst_pos_index], dim=0)
66
+ neg_edges = torch.stack([batch.src_index, batch.dst_neg_index], dim=0)
67
+ b_edge_label_index = torch.cat([pos_edges, neg_edges], dim=1)
68
+
69
+ b_edge_label = torch.cat([
70
+ torch.ones(batch.src_index.size(0), device=device),
71
+ torch.zeros(batch.src_index.size(0), device=device)
72
+ ])
73
+
74
+ # One-hot encoding of features
75
+ batch.x = F.one_hot(batch.x, num_classes=num_classes).float().squeeze(1)
76
+
77
+ # Forward pass
78
+ h = model(batch.x, batch.edge_index)
79
+ h_src, h_dst = h[b_edge_label_index[0]], h[b_edge_label_index[1]]
80
+ pred = (h_src * h_dst).sum(dim=-1)
81
+
82
+ probabilities = torch.sigmoid(pred)
83
+ predictions = (probabilities >= 0.5).float()
84
+
85
+ acc = (predictions == b_edge_label).float().mean().item()
86
+ batch_acc.append(acc)
87
+
88
+ del batch # free GPU memory
89
+
90
+ all_acc.append(np.mean(batch_acc))
91
+
92
+ del val_mini_loader, d_batch
93
+
94
+ return float(np.mean(all_acc))
95
+
96
+ def train(
97
+ model,
98
+ train_loader,
99
+ device,
100
+ num_classes: int,
101
+ optimizer =None,
102
+ batch_size: int = 2048,
103
+ num_neighbors: list[int] = [20, 10],
104
+ verbose: bool = True,
105
+ num_train_edges: int = 5000,
106
+ seed: int = 42
107
+
108
+ ):
109
+ """
110
+ Run one epoch of training for link prediction.
111
+
112
+ Args:
113
+ model (torch.nn.Module): The GNN model to train.
114
+ train_loader (torch.utils.data.DataLoader): Loader with training graphs.
115
+ device (torch.device): Device for computation.
116
+ num_classes (int): Number of node feature classes (for one-hot encoding).Equals the number of unique gene ids in all samples.
117
+ optimizer: Torch optimizer, eg. torch.optim.Adam(model.parameters(), lr=0.001)
118
+ batch_size (int, optional): Mini-batch size. Defaults to 2048.
119
+ num_neighbors (list[int], optional): Neighbor sampling sizes. Defaults to [20, 10].
120
+ verbose (bool, optional): Show progress bars. Defaults to True.
121
+
122
+ Returns:
123
+ tuple[float, float]: Mean loss and mean accuracy across batches.
124
+ """
125
+ rn.seed(seed)
126
+ model.train()
127
+ d_batch_loss, d_batch_acc = [], []
128
+
129
+ for d_i, d_batch in enumerate(train_loader):
130
+ if verbose:
131
+ print(f"Training on batch graph {d_i}...")
132
+ sampled_edge_index = getattr(d_batch, "edge_label_index", None)
133
+ if sampled_edge_index is None:
134
+ sampled_edge_index = d_batch.edge_index[:,rn.sample(range(d_batch.edge_index.shape[1]),num_train_edges)]
135
+
136
+ train_mini_loader = LinkNeighborLoader(
137
+ d_batch,
138
+ batch_size=batch_size,
139
+ shuffle=True,
140
+ edge_label_index=sampled_edge_index,
141
+ neg_sampling=NegativeSampling(mode="triplet", amount=1),
142
+ num_neighbors=num_neighbors,
143
+ disjoint=False,
144
+ subgraph_type="bidirectional",
145
+ )
146
+
147
+ total_loss, correct, total_edges = 0, 0, 0
148
+
149
+ for batch in tqdm(train_mini_loader, desc=f"Train minibatching {d_i}", disable=not verbose):
150
+ batch = batch.to(device)
151
+ optimizer.zero_grad()
152
+
153
+ # Edge labels
154
+ pos_labels = torch.ones(batch.src_index.size(0), device=device)
155
+ neg_labels = torch.zeros(batch.src_index.size(0), device=device)
156
+ b_edge_label = torch.cat([pos_labels, neg_labels])
157
+ b_edge_label_index = torch.cat([
158
+ torch.stack([batch.src_index, batch.dst_pos_index]),
159
+ torch.stack([batch.src_index, batch.dst_neg_index])
160
+ ], dim=1)
161
+
162
+ # Forward
163
+ batch.x = F.one_hot(batch.x, num_classes=num_classes).float().squeeze(1)
164
+ h = model(batch.x, batch.edge_index)
165
+ h_src, h_dst = h[b_edge_label_index[0]], h[b_edge_label_index[1]]
166
+ pred = (h_src * h_dst).sum(dim=-1)
167
+
168
+ # Loss
169
+ loss = F.binary_cross_entropy_with_logits(pred, b_edge_label)
170
+ loss.backward()
171
+ optimizer.step()
172
+
173
+ # Metrics
174
+ total_loss += loss.item() * pred.size(0)
175
+ probs = torch.sigmoid(pred)
176
+ preds = (probs >= 0.5).float()
177
+ correct += (preds == b_edge_label).sum().item()
178
+ total_edges += pred.size(0)
179
+
180
+ del batch # free GPU memory
181
+
182
+ d_batch_loss.append(total_loss / total_edges)
183
+ d_batch_acc.append(correct / total_edges)
184
+
185
+ del train_mini_loader, d_batch
186
+
187
+
188
+ return float(np.mean(d_batch_loss)), float(np.mean(d_batch_acc))
189
+
190
+ def train_label_pred(
191
+ model,
192
+ train_loader,
193
+ device,
194
+ num_classes: int,
195
+ optimizer = None,
196
+ batch_size: int = 2048,
197
+ num_neighbors: list[int] = [20, 10],
198
+ verbose: bool = True,
199
+ num_train_nodes: int = 10000,
200
+ seed: int = 42
201
+ ):
202
+ """
203
+ Run one epoch of training for link prediction.
204
+
205
+ Args:
206
+ model (torch.nn.Module): The GNN model to train.
207
+ train_loader (torch.utils.data.DataLoader): Loader with training graphs.
208
+ device (torch.device): Device for computation.
209
+ num_classes (int): Number of node feature classes (for one-hot encoding).Equals the number of unique gene ids in all samples.
210
+ optimizer:Torch optimier, eg. torch.optim.Adam(model.parameters(), lr=0.001)
211
+ batch_size (int, optional): Mini-batch size. Defaults to 2048.
212
+ num_neighbors (list[int], optional): Neighbor sampling sizes. Defaults to [20, 10].
213
+ verbose (bool, optional): Show progress bars. Defaults to True.
214
+
215
+ Returns:
216
+ tuple[float, float]: Mean loss and mean accuracy across batches.
217
+ """
218
+ rn.seed(seed)
219
+ model.train()
220
+ batch_losses, batch_accuracies = [], []
221
+
222
+ for d_i, d_batch in enumerate(train_loader):
223
+ if verbose:
224
+ print(f"Training on batch graph {d_i}...")
225
+
226
+ train_mask = getattr(d_batch, "train_mask", None)
227
+ if train_mask is None:
228
+ # random subset of nodes for training
229
+ train_mask = torch.tensor(
230
+ rn.sample(range(d_batch.x.shape[0]), num_train_nodes),
231
+ dtype=torch.long
232
+ )
233
+ else:
234
+ train_mask = train_mask.nonzero(as_tuple=False).view(-1)
235
+
236
+ # Create mini-batches with neighbor sampling
237
+ train_mini_loader = NeighborLoader(
238
+ d_batch,
239
+ batch_size=batch_size,
240
+ shuffle=True,
241
+ num_neighbors=num_neighbors,
242
+ input_nodes=train_mask,
243
+ subgraph_type = "bidirectional"
244
+ )
245
+
246
+ for batch in tqdm(train_mini_loader, disable=not verbose):
247
+ batch = batch.to(device)
248
+ optimizer.zero_grad()
249
+ batch.x = F.one_hot(batch.x, num_classes=num_classes).float().squeeze(1)
250
+ # Restrict loss computation to labeled nodes in the mini-batch. Head nodes of batch_size
251
+ out = model(batch.x, batch.edge_index)[:batch.batch_size]
252
+ y_head = batch.y[:batch.batch_size]
253
+
254
+ loss = F.cross_entropy(out, y_head)
255
+ loss.backward()
256
+ optimizer.step()
257
+
258
+ batch_losses.append(loss.item())
259
+ preds = out.argmax(dim=1)
260
+ correct = (preds == y_head).sum().item()
261
+ acc = correct / y_head.size(0)
262
+ batch_accuracies.append(acc)
263
+
264
+ mean_loss = float(sum(batch_losses) / len(batch_losses))
265
+ mean_acc = float(sum(batch_accuracies) / len(batch_accuracies))
266
+ return mean_loss, mean_acc
267
+
268
+
269
+
270
+ @torch.no_grad
271
+ def inference(
272
+ model,
273
+ device,
274
+ sample_name,
275
+ root = "../data/",
276
+ tile_id = [0],
277
+ num_neighbors = [20,10],
278
+ num_classes: int = 500,
279
+ batch_size: int = 2048,
280
+ verbose = True) -> None:
281
+ """
282
+ Run one epoch of training for link prediction.
283
+
284
+ Args:
285
+ model (torch.nn.Module): The GNN model with trained weights.
286
+ device (torch.device): Device for computation.
287
+ sample_name (str): sample name.
288
+ tile_id ([int]): The tile ids to process for the sample_name.
289
+ num_classes (int): Number of node feature classes (for one-hot encoding).Equals the number of unique gene ids in all samples.
290
+ batch_size (int, optional): Mini-batch size. Defaults to 2048.
291
+ num_neighbors (list[int], optional): Neighbor sampling sizes. Defaults to [20, 10].
292
+ verbose (bool, optional): Show progress bars. Defaults to True.
293
+ """
294
+ for t_id in tile_id:
295
+ data_path = osp.join(root,"processed",f'{sample_name}_data_tile{t_id}.pt')
296
+ assert osp.exists(data_path)
297
+ data = torch.load(data_path)
298
+ data = Data(**data[0])
299
+ subgraph_loader = NeighborLoader(
300
+ data,
301
+ input_nodes=data.core_mask,
302
+ #num_neighbors=[-1],
303
+ num_neighbors=num_neighbors,
304
+ batch_size=batch_size,
305
+ replace=False,
306
+ shuffle=False,
307
+ subgraph_type = "bidirectional"
308
+ )
309
+ xs = []
310
+ for batch in tqdm(subgraph_loader, disable=not verbose):
311
+ batch = batch.to(device)
312
+ batch.x = torch.nn.functional.one_hot(batch.x, num_classes=num_classes).float().squeeze(1)
313
+
314
+ out = model(batch.x, batch.edge_index)
315
+ # NOTE Only consider predictions and labels of seed nodes:
316
+ out = out[:batch.batch_size]
317
+ xs.append(out.cpu())
318
+
319
+ x_all = torch.cat(xs, dim=0)
320
+ # Define directory for embeddings
321
+ emb_dir = osp.join(root, "embedding")
322
+ os.makedirs(emb_dir, exist_ok=True)
323
+ # Define output .npy file path
324
+ out_embs_npy = osp.join(emb_dir, f"{sample_name}_data_tile{t_id}.npy")
325
+ np.save(out_embs_npy,x_all)
326
+ pd.DataFrame({"tx_id":data.trans_id[data.core_mask.cpu()]}).to_csv(out_embs_npy.replace(".npy","input_tx_id.csv"), index=False)
327
+ None
328
+
329
+
330
+ import torch
331
+ from torch_geometric.loader import NeighborLoader
332
+ from tqdm import tqdm
333
+
334
+ def validate_label_pred(
335
+ model,
336
+ val_loader,
337
+ device,
338
+ batch_size: int = 2048,
339
+ num_neighbors: list[int] = [20, 10],
340
+ verbose: bool = True
341
+ ):
342
+ """
343
+ Run one epoch of validation for node classification.
344
+
345
+ Args:
346
+ model (torch.nn.Module): Trained GNN model.
347
+ val_loader (torch.utils.data.DataLoader): Loader with validation graphs.
348
+ device (torch.device): Device for computation.
349
+ batch_size (int, optional): Mini-batch size. Defaults to 2048.
350
+ num_neighbors (list[int], optional): Neighbor sampling sizes. Defaults to [20, 10].
351
+ verbose (bool, optional): Show progress bars. Defaults to True.
352
+
353
+ Returns:
354
+ float: Mean accuracy across validation batches.
355
+ """
356
+ model.eval()
357
+ batch_accuracies = []
358
+
359
+ with torch.no_grad():
360
+ for d_i, d_batch in enumerate(val_loader):
361
+ if verbose:
362
+ print(f"Validating on batch graph {d_i}...")
363
+
364
+ val_mask = getattr(d_batch, "val_mask", None)
365
+ if val_mask is not None:
366
+ val_nodes = val_mask.nonzero(as_tuple=False).view(-1)
367
+
368
+ val_mini_loader = NeighborLoader(
369
+ d_batch,
370
+ batch_size=batch_size,
371
+ shuffle=False,
372
+ num_neighbors=num_neighbors,
373
+ input_nodes=val_nodes,
374
+ )
375
+
376
+ for batch in tqdm(val_mini_loader, disable=not verbose):
377
+ batch = batch.to(device)
378
+ out = model(batch.x, batch.edge_index)[:batch.batch_size]
379
+ y_head = batch.y[:batch.batch_size]
380
+
381
+ preds = out.argmax(dim=1)
382
+ correct = (preds == y_head).sum().item()
383
+ acc = correct / y_head.size(0)
384
+ batch_accuracies.append(acc)
385
+
386
+ mean_acc = float(sum(batch_accuracies) / len(batch_accuracies))
387
+ return mean_acc
@@ -0,0 +1,188 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import matplotlib.colors as mcolors
5
+ import os
6
+ import os.path as osp
7
+ from scipy.stats import mode
8
+ import cv2
9
+
10
+ def get_tx_plot_df(root="multi_sample_label_pred",
11
+ sample_name ="lung1",
12
+ n_clusters = 10):
13
+ """
14
+ Gather clustering results and transcript meta data into one DataFrame
15
+
16
+ Args:
17
+ root (str): Path of root data dir. Transcript meta file is expected in {root}/sample_name/raw/sample_name.csv
18
+ n_clusters: The number of K clusters in previous run_kmeans function call
19
+ """
20
+ tx_meta_file = osp.join(root,sample_name,"raw",f"{sample_name}.csv")
21
+ assert osp.exists(tx_meta_file)
22
+ tx_meta = pd.read_csv(tx_meta_file)
23
+ tx_meta = tx_meta.reset_index(drop=True)
24
+ tx_meta["tx_ids"] = tx_meta.index
25
+
26
+ cluster_res = osp.join(root,sample_name,"clusters",f"{sample_name}.{n_clusters}clusters.csv")
27
+ assert osp.exists(cluster_res)
28
+ cluster_res_df = pd.read_csv(cluster_res)
29
+ # --- Reorder cluster_res_df to match tx_meta.index ---
30
+ cluster_res_df = cluster_res_df.set_index("tx_ids").loc[tx_meta.index].reset_index()
31
+
32
+ # Now assign labels without reordering tx_meta
33
+ tx_meta["cluster_labels"] = cluster_res_df["cluster_labels"].values
34
+
35
+ return tx_meta
36
+ def plot_pixel(tx_meta,
37
+ pixel_size: float = 10,
38
+ min_points: int = 5,
39
+ x='X',
40
+ y='Y',
41
+ cluster_labels="cluster_labels",
42
+ figsize=(8, 8),
43
+ cmap=None,
44
+ join_method="avg", # "avg" or "major"
45
+ background_color="white",
46
+ output_path=None,
47
+ **kwargs):
48
+ """
49
+ Pixelate points and color each pixel by either:
50
+ - average RGB of all points ("avg"), or
51
+ - major label color ("major").
52
+ Pixels with < min_points are set to background.
53
+ """
54
+ if not isinstance(cmap, dict):
55
+ raise ValueError("cmap must be a dict mapping cluster labels to hex colors")
56
+ if join_method not in {"avg", "major"}:
57
+ raise ValueError("join_method must be 'avg' or 'major'")
58
+
59
+ tx_meta = tx_meta.copy()
60
+
61
+ # Map cluster labels → RGB (0-1 range)
62
+ tx_meta['RGB'] = tx_meta[cluster_labels].map(
63
+ lambda lbl: mcolors.to_rgb(cmap.get(lbl, background_color))
64
+ )
65
+ tx_meta[['R', 'G', 'B']] = pd.DataFrame(tx_meta['RGB'].tolist(), index=tx_meta.index)
66
+
67
+ # Compute pixel indices
68
+ tx_meta['px'] = (tx_meta[x] // pixel_size).astype(int)
69
+ tx_meta['py'] = (tx_meta[y] // pixel_size).astype(int)
70
+
71
+ # Function to compute pixel color per group
72
+ def pixel_color(df):
73
+ if len(df) < min_points:
74
+ return pd.Series([np.nan, np.nan, np.nan], index=['R','G','B'])
75
+ if join_method == "avg":
76
+ return pd.Series([df['R'].mean(), df['G'].mean(), df['B'].mean()], index=['R','G','B'])
77
+ else: # "major"
78
+ major_label = df[cluster_labels].mode().iloc[0]
79
+ return pd.Series(mcolors.to_rgb(cmap.get(major_label, background_color)), index=['R','G','B'])
80
+
81
+
82
+ # Only keep the columns needed by pixel_color
83
+ subset = tx_meta[['px', 'py', 'R', 'G', 'B', cluster_labels]]
84
+ # Group by px, py and apply pixel_color
85
+ pixel_df = (
86
+ subset.groupby(['px', 'py'])[['R','G','B', cluster_labels]]
87
+ .apply(pixel_color)
88
+ .reset_index()
89
+ )
90
+ # Pivot into 2D grid
91
+ pivot_r = pixel_df.pivot(index='py', columns='px', values='R').iloc[::-1]
92
+ pivot_g = pixel_df.pivot(index='py', columns='px', values='G').iloc[::-1]
93
+ pivot_b = pixel_df.pivot(index='py', columns='px', values='B').iloc[::-1]
94
+
95
+ img = np.dstack([pivot_r, pivot_g, pivot_b])
96
+
97
+ # Fill NaNs with background color
98
+ bg_rgb = np.array(mcolors.to_rgb(background_color))
99
+ nan_mask = np.isnan(img)
100
+ img[nan_mask] = np.take(bg_rgb, nan_mask.nonzero()[2])
101
+
102
+ if output_path is None:
103
+ fig, ax = plt.subplots(1, 1, figsize=figsize, **kwargs)
104
+ ax.imshow(img, interpolation="none", origin="upper")
105
+ ax.axis("off")
106
+ ax.set_title(f"Pixelated ({join_method}) (min_points={min_points})")
107
+ return fig, ax
108
+ else:
109
+ # Convert to uint8 and BGR for OpenCV
110
+ img_uint8 = np.clip(img*255, 0, 255).astype(np.uint8)
111
+ img_bgr = cv2.cvtColor(img_uint8, cv2.COLOR_RGB2BGR)
112
+ cv2.imwrite(output_path, img_bgr)
113
+ print(f"Saved pixelated RGB image to {output_path}")
114
+
115
+
116
+ def plot_hex_bin(tx_meta,
117
+ x="X",
118
+ y="Y",
119
+ cluster_labels="cluster_labels",
120
+ bin_thresh=10,
121
+ bin_width=5,
122
+ figsize=(12, 10),
123
+ cmap=None,
124
+ background_color="white",
125
+ **kwargs):
126
+ """
127
+ Hex-binning of points, colored by majority cluster label.
128
+ """
129
+
130
+ # majority rule
131
+ def get_major_cluster(values):
132
+ if len(values) < bin_thresh:
133
+ return np.nan
134
+ result = mode(values, keepdims=True)
135
+ return result.mode[0] if result.mode.size > 0 else np.nan
136
+
137
+ # bin resolution
138
+ x_bins = int((tx_meta[x].max() - tx_meta[x].min()) / bin_width)
139
+ y_bins = int((tx_meta[y].max() - tx_meta[y].min()) / bin_width)
140
+
141
+ fig, ax = plt.subplots(figsize=figsize, constrained_layout=True, **kwargs)
142
+
143
+ # handle cmap (dict or colormap string)
144
+ if isinstance(cmap, dict):
145
+ # map labels → ints for hexbin
146
+ labels_unique = sorted(tx_meta[cluster_labels].dropna().unique())
147
+ label_to_int = {lbl: i for i, lbl in enumerate(labels_unique)}
148
+ int_to_color = {label_to_int[k]: mcolors.to_rgba(v) for k, v in cmap.items()}
149
+ # convert data to ints
150
+ data_vals = tx_meta[cluster_labels].map(label_to_int)
151
+ cmap_obj = mcolors.ListedColormap([int_to_color.get(i, mcolors.to_rgba(background_color))
152
+ for i in range(len(label_to_int))])
153
+
154
+ else:
155
+ # numeric encoding for labels
156
+ labels_unique = sorted(tx_meta[cluster_labels].dropna().unique())
157
+ label_to_int = {lbl: i for i, lbl in enumerate(labels_unique)}
158
+ data_vals = tx_meta[cluster_labels].map(label_to_int)
159
+ cmap_obj = plt.get_cmap(cmap or "tab10", len(labels_unique))
160
+
161
+ # hexbin majority aggregation
162
+ hb = ax.hexbin(
163
+ tx_meta[x],
164
+ tx_meta[y],
165
+ C=data_vals,
166
+ gridsize=(x_bins, y_bins),
167
+ reduce_C_function=get_major_cluster,
168
+ linewidths=0.05,
169
+ cmap=cmap_obj
170
+ )
171
+
172
+ # axis cosmetics
173
+ ax.invert_yaxis()
174
+ ax.set_xlabel(x, fontsize=12)
175
+ ax.set_ylabel(y, fontsize=12)
176
+ ax.tick_params(axis='both', which='both', colors='black')
177
+ for spine in ["top", "right"]:
178
+ ax.spines[spine].set_visible(False)
179
+ for spine in ["left", "bottom"]:
180
+ ax.spines[spine].set_linewidth(1.5)
181
+ ax.grid(False)
182
+ ax.set_aspect('equal', adjustable='box') # or 'datalim'
183
+
184
+ # colorbar
185
+ cb = fig.colorbar(hb, ax=ax, orientation="vertical")
186
+ cb.set_label("Major Cluster", fontsize=10)
187
+
188
+ return fig, ax
@@ -1,15 +1,16 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: spatialrna
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: A package for spatial RNA graph data processing and analysis.
5
5
  Author-email: Ruqian Lyu <rlyu@svi.edu.au>
6
6
  License: MIT
7
- Project-URL: repository, https://gitlab.svi.edu.au/biocellgen-private/spatialrna
7
+ Project-URL: repository, https://github.com/ruqianl/spatialrna.git
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
+ License-File: LICENSE.txt
13
14
  Requires-Dist: torch
14
15
  Requires-Dist: torch_geometric
15
16
  Requires-Dist: pandas
@@ -18,6 +19,7 @@ Provides-Extra: dev
18
19
  Requires-Dist: pytest>=6.0; extra == "dev"
19
20
  Requires-Dist: twine>=4.0.2; extra == "dev"
20
21
  Requires-Dist: build; extra == "dev"
22
+ Dynamic: license-file
21
23
 
22
24
  ## A package that provides easier inferface to GNN models for single-molecule ST data
23
25
 
@@ -43,6 +45,14 @@ pip install torch_geometric==2.6.1
43
45
 
44
46
  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
47
 
48
+ ## clustering and plotting requires
49
+
50
+ pip install tqdm \
51
+ scipy \
52
+ scikit-learn \
53
+ pandas \
54
+ matplotlib \
55
+ opencv-python
46
56
  ```
47
57
 
48
58
  ## Install spatialrna
@@ -50,3 +60,7 @@ pip install pyg_lib torch_scatter torch_sparse torch_cluster torch_spline_conv -
50
60
  ```
51
61
  pip install spatialrna
52
62
  ```
63
+
64
+ ## Documentations
65
+
66
+ https://ruqianl.github.io/spatialrna_docs/
@@ -1,7 +1,12 @@
1
+ LICENSE.txt
1
2
  README.md
2
3
  pyproject.toml
3
4
  spatialrna/__init__.py
5
+ spatialrna/run_kmeans.py
4
6
  spatialrna/spatialrna.py
7
+ spatialrna/spatialrna_ondisk.py
8
+ spatialrna/train_val.py
9
+ spatialrna/viz.py
5
10
  spatialrna.egg-info/PKG-INFO
6
11
  spatialrna.egg-info/SOURCES.txt
7
12
  spatialrna.egg-info/dependency_links.txt
File without changes