hutaobo commited on
Commit
fe6d94c
·
verified ·
1 Parent(s): 7e54905

Correct public repo scope and remove legacy CCST files

Browse files
Files changed (10) hide show
  1. .gitattributes +35 -35
  2. .gitignore +4 -4
  3. CITATION.cff +16 -16
  4. LICENSE.md +7 -7
  5. MERGE_NOTES.md +0 -50
  6. README.md +28 -111
  7. references/REFERENCE_NOTE.md +0 -5
  8. requirements.txt +0 -10
  9. run_ccst.py +0 -432
  10. upload_to_hf.py +52 -52
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -1,4 +1,4 @@
1
- __pycache__/
2
- *.pyc
3
- .DS_Store
4
- outputs/
 
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
4
+ outputs/
CITATION.cff CHANGED
@@ -1,31 +1,31 @@
1
  cff-version: 1.2.0
2
- message: "If you use this repository, please cite the associated manuscript and this repository."
3
- title: "CCST for Spatial Transcriptomics"
4
  type: software
5
  authors:
6
  - family-names: "Hu"
7
  given-names: "Taobo"
8
- - family-names: "Long"
9
- given-names: "Mengping"
10
  repository-code: "https://huggingface.co/hutaobo/ccst-spatial-clustering"
11
  license: "CC-BY-NC-4.0"
12
  keywords:
13
- - spatial transcriptomics
14
- - PyTorch Geometric
15
- - graph neural network
16
- - Visium
17
  preferred-citation:
18
  type: article
19
  title: "Cophenetic Spatial Topology Embedding reveals multiscale tissue architecture in spatial omics"
20
- authors:
21
- - family-names: "Long"
22
- given-names: "Mengping"
23
- - family-names: "Hu"
24
- given-names: "Taobo"
25
- - family-names: "Sountoulidis"
26
- given-names: "Alexandros"
27
  - family-names: "Samakovlis"
28
  given-names: "Christos"
29
  - family-names: "Nilsson"
30
  given-names: "Mats"
31
- note: "Included in this repository as a user-provided reference PDF."
 
1
  cff-version: 1.2.0
2
+ message: "Please cite the associated manuscript for scientific results and cite this repository as a manuscript companion placeholder when relevant."
3
+ title: "COSTE and DST-GNN Manuscript Companion Placeholder"
4
  type: software
5
  authors:
6
  - family-names: "Hu"
7
  given-names: "Taobo"
8
+ - family-names: "Long"
9
+ given-names: "Mengping"
10
  repository-code: "https://huggingface.co/hutaobo/ccst-spatial-clustering"
11
  license: "CC-BY-NC-4.0"
12
  keywords:
13
+ - spatial omics
14
+ - tissue architecture
15
+ - manuscript companion
16
+ abstract: "Public manuscript companion placeholder for the COSTE and DST-GNN study. The current repository release does not include the DST-GNN implementation used for manuscript analyses."
17
  preferred-citation:
18
  type: article
19
  title: "Cophenetic Spatial Topology Embedding reveals multiscale tissue architecture in spatial omics"
20
+ authors:
21
+ - family-names: "Long"
22
+ given-names: "Mengping"
23
+ - family-names: "Hu"
24
+ given-names: "Taobo"
25
+ - family-names: "Sountoulidis"
26
+ given-names: "Alexandros"
27
  - family-names: "Samakovlis"
28
  given-names: "Christos"
29
  - family-names: "Nilsson"
30
  given-names: "Mats"
31
+ note: "Scientific results should be attributed to the manuscript. The public DST-GNN implementation is not included in the current repository release."
LICENSE.md CHANGED
@@ -1,9 +1,9 @@
1
- CCST for Spatial Transcriptomics
2
  Non-Commercial Distribution Notice
3
 
4
- This repository is intended for non-commercial research and academic use.
5
 
6
- Requested Hugging Face license tag:
7
 
8
  - `cc-by-nc-4.0`
9
 
@@ -17,10 +17,10 @@ Official license page:
17
 
18
  Summary:
19
 
20
- - You may share and adapt the material for non-commercial purposes.
21
- - Appropriate attribution should be given when redistributing or adapting this repository.
22
  - Commercial use is not permitted under this repository-level release setting.
23
 
24
- Important note:
25
 
26
- This repository was assembled from local CCST script snapshots and user-provided reference material. If any upstream project imposes additional terms, those upstream terms should also be respected.
 
1
+ COSTE and DST-GNN Manuscript Companion Placeholder
2
  Non-Commercial Distribution Notice
3
 
4
+ The contents currently published in this repository are released for non-commercial research and academic use.
5
 
6
+ Repository license tag:
7
 
8
  - `cc-by-nc-4.0`
9
 
 
17
 
18
  Summary:
19
 
20
+ - You may share and adapt the currently published repository contents for non-commercial purposes.
21
+ - Appropriate attribution should be given when redistributing or adapting the repository contents.
22
  - Commercial use is not permitted under this repository-level release setting.
23
 
24
+ Scope note:
25
 
26
+ The current public release is a manuscript companion placeholder consisting of descriptive documentation and repository metadata. The DST-GNN implementation described in the associated manuscript is not included in this release.
MERGE_NOTES.md DELETED
@@ -1,50 +0,0 @@
1
- # Merge Notes
2
-
3
- ## Source Files
4
-
5
- This repository was assembled from the following local files:
6
-
7
- - `C:\Users\taobo.hu\Downloads\SRTBenchmark-main\SRTBenchmark-main\Methods\CCST_Sample.py`
8
- - `C:\Users\taobo.hu\Downloads\SRTBenchmark-main\SRTBenchmark-main\Methods\CCST_Sample_Optimized.py`
9
-
10
- ## Main Observed Difference
11
-
12
- The local diff between the two scripts was limited to preprocessing:
13
-
14
- ```text
15
- CCST_Sample.py
16
- - features = adata_preprocess(adata, min_cells=5, pca_n_comps=200)
17
-
18
- CCST_Sample_Optimized.py
19
- - sc.pp.highly_variable_genes(adata, flavor='seurat_v3', n_top_genes=2000)
20
- - adata = adata[:, adata.var.highly_variable]
21
- - features = adata_preprocess(adata, min_cells=5, pca_n_comps=50)
22
- ```
23
-
24
- ## Cleanup Performed
25
-
26
- - Merged duplicated logic into one script: `run_ccst.py`
27
- - Added CLI arguments for data paths and preprocessing choices
28
- - Replaced hard-coded paths with arguments
29
- - Kept both preprocessing behaviors through profiles
30
- - Added output summary JSON
31
- - Made the Leiden/Louvain branch executable instead of relying on undefined names
32
- - Removed unused imports and tightened the dependency surface
33
-
34
- ## Reference PDF
35
-
36
- During local preparation, the following user-provided manuscript PDF was consulted:
37
-
38
- - `references/719013_0_art_file_252332_t72882.pdf`
39
-
40
- Extracted first-page title:
41
-
42
- - `Cophenetic Spatial Topology Embedding reveals multiscale tissue architecture in spatial omics`
43
-
44
- This PDF was used as a local reference artifact during repository preparation.
45
- It is not intended to be uploaded as part of the public Hugging Face repository.
46
-
47
- ## Planned Hub Metadata
48
-
49
- - Recommended repo name: `hutaobo/ccst-spatial-clustering`
50
- - Requested Hugging Face license tag: `cc-by-nc-4.0`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,36 +1,31 @@
1
  ---
2
  license: cc-by-nc-4.0
3
- library_name: pytorch
4
  tags:
5
- - spatial-transcriptomics
6
- - graph-neural-network
7
- - torch-geometric
8
- - visium
9
  - research
 
 
 
10
  ---
11
 
12
- # CCST for Spatial Transcriptomics
13
 
14
- This repository contains a cleaned, merged version of two local CCST scripts:
15
 
16
- - `CCST_Sample.py`
17
- - `CCST_Sample_Optimized.py`
18
 
19
- The merged implementation is provided as `run_ccst.py`.
20
 
21
- Recommended Hugging Face repository name:
 
 
22
 
23
- - `hutaobo/ccst-spatial-clustering`
24
 
25
- This repository is intended as a compact, publication-oriented companion repository for CCST-based spatial clustering workflows implemented with PyTorch Geometric.
26
-
27
- ## Research Context
28
-
29
- Reference manuscript used while preparing this repository:
30
 
31
  - *Cophenetic Spatial Topology Embedding reveals multiscale tissue architecture in spatial omics*
32
 
33
- Authors listed on the first page of the provided PDF:
34
 
35
  - Mengping Long
36
  - Taobo Hu
@@ -38,107 +33,29 @@ Authors listed on the first page of the provided PDF:
38
  - Christos Samakovlis
39
  - Mats Nilsson
40
 
41
- ## What Was Merged
42
-
43
- The original local scripts were nearly identical. The main preprocessing difference was:
44
-
45
- - `CCST_Sample.py`: no highly variable gene filtering, PCA with 200 components
46
- - `CCST_Sample_Optimized.py`: HVG filtering with `n_top_genes=2000`, PCA with 50 components
47
-
48
- The merged script keeps both behaviors through a single command-line interface:
49
-
50
- - `--profile original`
51
- - `--profile optimized`
52
-
53
- You can also override the defaults with:
54
-
55
- - `--use-hvg` or `--no-hvg`
56
- - `--n-top-genes`
57
- - `--pca-n-comps`
58
-
59
- ## Repository Layout
60
-
61
- - `run_ccst.py`: merged and cleaned CCST runner
62
- - `requirements.txt`: Python dependencies
63
- - `MERGE_NOTES.md`: provenance and merge notes
64
- - `LICENSE.md`: non-commercial repository license note
65
- - `CITATION.cff`: citation metadata for scholarly reuse
66
- - `references/REFERENCE_NOTE.md`: note about the manuscript reference used during repository preparation
67
-
68
- ## Usage
69
 
70
- Example using the optimized profile:
71
 
72
- ```bash
73
- python run_ccst.py \
74
- --visium-dir ./DLPFC/151673 \
75
- --output-dir ./outputs/151673 \
76
- --profile optimized
77
- ```
78
 
79
- Example reproducing the original local preprocessing:
80
 
81
- ```bash
82
- python run_ccst.py \
83
- --visium-dir ./DLPFC/151673 \
84
- --output-dir ./outputs/151673_original \
85
- --profile original
86
- ```
87
 
88
- Example with explicit overrides:
 
 
89
 
90
- ```bash
91
- python run_ccst.py \
92
- --visium-dir ./DLPFC/151673 \
93
- --output-dir ./outputs/151673_custom \
94
- --profile optimized \
95
- --no-hvg \
96
- --pca-n-comps 120 \
97
- --distance-threshold 200 \
98
- --lambda-i 0.3
99
- ```
100
-
101
- ## Expected Inputs
102
-
103
- The script expects a standard 10x Visium-style sample directory with:
104
-
105
- - a count matrix file, default: `filtered_feature_bc_matrix.h5`
106
- - a metadata table, default: `metadata.tsv`
107
-
108
- The metadata file should include a ground-truth annotation column. By default this script uses:
109
-
110
- - `layer_guess`
111
-
112
- You can change that with:
113
-
114
- ```bash
115
- --ground-truth-column your_column_name
116
- ```
117
-
118
- ## Outputs
119
-
120
- The output directory will contain:
121
-
122
- - `features.npy`
123
- - `coordinates.npy`
124
- - `Adjacent`
125
- - `cell_types.npy`
126
- - `types_dic.txt`
127
- - DGI checkpoint
128
- - node embedding `.npy`
129
- - `CCST_results.h5ad`
130
- - `run_summary.json`
131
 
132
  ## Citation
133
 
134
- If you use this repository in academic work, please cite the associated manuscript and reference this repository as the script companion used for CCST preprocessing and clustering reproduction.
135
-
136
- The repository includes a `CITATION.cff` file to support software citation workflows.
137
 
138
- ## Notes
139
 
140
- - The merged script defaults to `kmeans`, because that is the branch actually exercised by the local scripts.
141
- - `leiden` and `louvain` are also supported in the cleaned script to make the clustering step explicit and self-contained.
142
- - The Hugging Face repo card is configured with `license: cc-by-nc-4.0` to reflect the requested non-commercial distribution setting on the Hub.
143
- - The local downloaded snapshot used to build this repo did not expose a clear software license file, so public redistribution should be reviewed carefully before release outside your intended publication workflow.
144
- - The manuscript PDF used during local preparation is intentionally not uploaded to the public Hugging Face repository.
 
1
  ---
2
  license: cc-by-nc-4.0
 
3
  tags:
 
 
 
 
4
  - research
5
+ - computational-biology
6
+ - spatial-omics
7
+ - manuscript-companion
8
  ---
9
 
10
+ # COSTE and DST-GNN Manuscript Companion
11
 
12
+ The repository path `hutaobo/ccst-spatial-clustering` is retained for continuity from an earlier private working name.
13
 
14
+ This public repository is currently a manuscript companion placeholder. The public release does **not** include the DST-GNN implementation used for the manuscript results.
 
15
 
16
+ ## Status
17
 
18
+ - This repository currently contains descriptive documentation and citation metadata only.
19
+ - No runnable modeling pipeline is included in the present public release.
20
+ - The current public contents should not be interpreted as the implementation behind the manuscript analyses.
21
 
22
+ ## Manuscript Context
23
 
24
+ This repository is associated with the manuscript:
 
 
 
 
25
 
26
  - *Cophenetic Spatial Topology Embedding reveals multiscale tissue architecture in spatial omics*
27
 
28
+ Authors listed on the manuscript title page:
29
 
30
  - Mengping Long
31
  - Taobo Hu
 
33
  - Christos Samakovlis
34
  - Mats Nilsson
35
 
36
+ The manuscript describes a workflow in which COSTE-derived spatial relationship graphs are analyzed over time with a diffusion-based spatio-temporal graph neural network (DST-GNN). That implementation is not part of the current public repository contents.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ ## What Is Currently Published
39
 
40
+ - `README.md`: public repository card and status summary
41
+ - `CITATION.cff`: citation metadata for the manuscript companion repository
42
+ - `LICENSE.md`: non-commercial distribution notice for the currently published repository contents
43
+ - `upload_to_hf.py`: utility script for synchronizing this repository to the Hugging Face Hub
 
 
44
 
45
+ ## Planned Public Release Scope
46
 
47
+ Future public updates may include:
 
 
 
 
 
48
 
49
+ - cohort-level COSTE spatial separation score graphs across multiple time points
50
+ - temporal graph modeling of tissue-state transitions
51
+ - explainability outputs for influential nodes and edges
52
 
53
+ No release timeline is promised in this repository.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  ## Citation
56
 
57
+ Please cite the associated manuscript for scientific results derived from COSTE and DST-GNN analyses. The repository also includes a `CITATION.cff` file for repository-level citation metadata.
 
 
58
 
59
+ ## License
60
 
61
+ The currently published repository contents are provided under a non-commercial license. See `LICENSE.md` for details.
 
 
 
 
references/REFERENCE_NOTE.md DELETED
@@ -1,5 +0,0 @@
1
- # Reference Note
2
-
3
- During local preparation of this repository, a user-provided manuscript PDF was consulted for title and authorship context.
4
-
5
- That manuscript file is not included in the public Hugging Face upload.
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,10 +0,0 @@
1
- anndata
2
- h5py
3
- matplotlib
4
- numpy
5
- pandas
6
- scanpy
7
- scikit-learn
8
- scipy
9
- torch
10
- torch-geometric
 
 
 
 
 
 
 
 
 
 
 
run_ccst.py DELETED
@@ -1,432 +0,0 @@
1
- #!/usr/bin/env python
2
- """Run CCST on a Visium sample with configurable preprocessing.
3
-
4
- This script merges two local variants:
5
- - CCST_Sample.py
6
- - CCST_Sample_Optimized.py
7
-
8
- The original difference was limited to preprocessing:
9
- - original: no HVG filtering, PCA to 200 components
10
- - optimized: HVG filtering (top 2000 genes), PCA to 50 components
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import argparse
16
- import json
17
- import os
18
- import pickle
19
- import random
20
- import time
21
- import tracemalloc
22
- from dataclasses import dataclass
23
- from pathlib import Path
24
-
25
- import anndata as ad
26
- import matplotlib
27
-
28
- matplotlib.use("Agg")
29
-
30
- import numpy as np
31
- import pandas as pd
32
- import scanpy as sc
33
- import torch
34
- import torch.nn as nn
35
- from scipy import sparse
36
- from sklearn import metrics
37
- from sklearn.cluster import KMeans
38
- from sklearn.decomposition import PCA
39
- from sklearn.metrics.pairwise import euclidean_distances
40
- from torch_geometric.data import Data
41
- from torch_geometric.loader import DataLoader
42
- from torch_geometric.nn import DeepGraphInfomax, GCNConv
43
-
44
-
45
- @dataclass
46
- class PreprocessingConfig:
47
- use_hvg: bool
48
- n_top_genes: int
49
- pca_n_comps: int
50
-
51
-
52
- class GraphEncoder(nn.Module):
53
- def __init__(self, in_channels: int, hidden_channels: int) -> None:
54
- super().__init__()
55
- self.conv_1 = GCNConv(in_channels, hidden_channels)
56
- self.conv_2 = GCNConv(hidden_channels, hidden_channels)
57
- self.conv_3 = GCNConv(hidden_channels, hidden_channels)
58
- self.conv_4 = GCNConv(hidden_channels, hidden_channels)
59
- self.prelu = nn.PReLU(hidden_channels)
60
-
61
- def forward(self, data: Data) -> torch.Tensor:
62
- x, edge_index, edge_weight = data.x, data.edge_index, data.edge_attr
63
- x = self.conv_1(x, edge_index, edge_weight=edge_weight)
64
- x = self.conv_2(x, edge_index, edge_weight=edge_weight)
65
- x = self.conv_3(x, edge_index, edge_weight=edge_weight)
66
- x = self.conv_4(x, edge_index, edge_weight=edge_weight)
67
- return self.prelu(x)
68
-
69
-
70
- class CorruptedGraph:
71
- def __init__(self, x: torch.Tensor, edge_index: torch.Tensor, edge_attr: torch.Tensor) -> None:
72
- self.x = x
73
- self.edge_index = edge_index
74
- self.edge_attr = edge_attr
75
-
76
-
77
- def set_seed(seed: int) -> None:
78
- random.seed(seed)
79
- np.random.seed(seed)
80
- torch.manual_seed(seed)
81
- if torch.cuda.is_available():
82
- torch.cuda.manual_seed_all(seed)
83
-
84
-
85
- def resolve_preprocessing(args: argparse.Namespace) -> PreprocessingConfig:
86
- if args.profile == "original":
87
- default = PreprocessingConfig(use_hvg=False, n_top_genes=2000, pca_n_comps=200)
88
- else:
89
- default = PreprocessingConfig(use_hvg=True, n_top_genes=2000, pca_n_comps=50)
90
-
91
- if args.use_hvg is not None:
92
- default.use_hvg = args.use_hvg
93
- if args.n_top_genes is not None:
94
- default.n_top_genes = args.n_top_genes
95
- if args.pca_n_comps is not None:
96
- default.pca_n_comps = args.pca_n_comps
97
- return default
98
-
99
-
100
- def preprocess_adata(
101
- adata: ad.AnnData,
102
- min_cells: int,
103
- config: PreprocessingConfig,
104
- ) -> tuple[np.ndarray, ad.AnnData]:
105
- print("===== Preprocessing data")
106
- sc.pp.filter_genes(adata, min_cells=min_cells)
107
-
108
- if config.use_hvg:
109
- sc.pp.highly_variable_genes(
110
- adata,
111
- flavor="seurat_v3",
112
- n_top_genes=config.n_top_genes,
113
- )
114
- adata = adata[:, adata.var.highly_variable].copy()
115
-
116
- normalized = sc.pp.normalize_total(
117
- adata,
118
- target_sum=1,
119
- exclude_highly_expressed=True,
120
- inplace=False,
121
- )["X"]
122
- scaled = sc.pp.scale(normalized)
123
- pca = sc.pp.pca(scaled, n_comps=config.pca_n_comps)
124
- return np.asarray(pca, dtype=np.float32), adata
125
-
126
-
127
- def build_adjacency_matrix(coordinates: np.ndarray, threshold: float) -> sparse.csr_matrix:
128
- print("===== Building adjacency matrix")
129
- distance_matrix = euclidean_distances(coordinates, coordinates)
130
- adjacency = ((distance_matrix <= threshold) & (distance_matrix > 0)).astype(np.float32)
131
- return sparse.csr_matrix(adjacency)
132
-
133
-
134
- def save_cell_types(cell_types: np.ndarray, output_dir: Path) -> None:
135
- seen: list[str] = []
136
- for cell_type in cell_types:
137
- as_text = str(cell_type)
138
- if as_text not in seen:
139
- seen.append(as_text)
140
-
141
- np.save(output_dir / "cell_types.npy", np.asarray(cell_types))
142
- np.savetxt(output_dir / "types_dic.txt", np.asarray(seen), fmt="%s", delimiter="\t")
143
-
144
-
145
- def load_graph_inputs(
146
- output_dir: Path,
147
- lambda_i: float,
148
- ) -> tuple[sparse.csr_matrix, sparse.csr_matrix, np.ndarray, np.ndarray]:
149
- with open(output_dir / "Adjacent", "rb") as handle:
150
- adjacency_raw = pickle.load(handle)
151
-
152
- features = np.load(output_dir / "features.npy")
153
- identity = sparse.identity(features.shape[0], dtype=np.float32, format="csr")
154
- adjacency = (1 - lambda_i) * adjacency_raw + lambda_i * identity
155
- cell_type_indices = np.load(output_dir / "cell_types.npy", allow_pickle=True)
156
- return adjacency_raw, adjacency, features, cell_type_indices
157
-
158
-
159
- def to_pyg_graph(adjacency: sparse.csr_matrix, features: np.ndarray) -> list[Data]:
160
- rows, cols = adjacency.nonzero()
161
- edge_index = torch.tensor(np.vstack([rows, cols]), dtype=torch.long)
162
- edge_attr = torch.tensor(adjacency.data, dtype=torch.float32)
163
- graph = Data(
164
- x=torch.tensor(features, dtype=torch.float32),
165
- edge_index=edge_index,
166
- edge_attr=edge_attr,
167
- )
168
- return [graph]
169
-
170
-
171
- def corruption(data: Data) -> CorruptedGraph:
172
- shuffled = data.x[torch.randperm(data.x.size(0))]
173
- return CorruptedGraph(shuffled, data.edge_index, data.edge_attr)
174
-
175
-
176
- def train_dgi(
177
- data_loader: DataLoader,
178
- in_channels: int,
179
- hidden_dim: int,
180
- epochs: int,
181
- learning_rate: float,
182
- device: torch.device,
183
- model_path: Path,
184
- ) -> DeepGraphInfomax:
185
- model = DeepGraphInfomax(
186
- hidden_channels=hidden_dim,
187
- encoder=GraphEncoder(in_channels=in_channels, hidden_channels=hidden_dim),
188
- summary=lambda z, *args, **kwargs: torch.sigmoid(z.mean(dim=0)),
189
- corruption=corruption,
190
- ).to(device)
191
- optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
192
- torch.backends.cudnn.enabled = False
193
-
194
- start_time = time.time()
195
- for epoch in range(epochs):
196
- model.train()
197
- losses: list[float] = []
198
- for batch in data_loader:
199
- batch = batch.to(device)
200
- optimizer.zero_grad()
201
- pos_z, neg_z, summary = model(data=batch)
202
- loss = model.loss(pos_z, neg_z, summary)
203
- loss.backward()
204
- optimizer.step()
205
- losses.append(float(loss.item()))
206
-
207
- if (epoch + 1) % 100 == 0:
208
- print(f"Epoch {epoch + 1:04d} | loss={np.mean(losses):.6f}")
209
-
210
- torch.save(model.state_dict(), model_path)
211
- print(f"Training time: {time.time() - start_time:.1f}s")
212
- return model
213
-
214
-
215
- def run_kmeans(embedding: np.ndarray, n_clusters: int, random_state: int) -> tuple[np.ndarray, float]:
216
- reducer = PCA(n_components=min(30, embedding.shape[1]))
217
- reduced = reducer.fit_transform(embedding)
218
- estimator = KMeans(
219
- n_clusters=n_clusters,
220
- init="k-means++",
221
- n_init=100,
222
- max_iter=1000,
223
- tol=1e-6,
224
- random_state=random_state,
225
- )
226
- labels = estimator.fit_predict(reduced)
227
- silhouette = metrics.silhouette_score(reduced, labels, metric="euclidean")
228
- return labels, float(silhouette)
229
-
230
-
231
- def search_fixed_resolution(
232
- cluster_type: str,
233
- adata: ad.AnnData,
234
- target_clusters: int,
235
- increment: float = 0.02,
236
- ) -> float:
237
- for resolution in sorted(np.arange(0.2, 2.5, increment), reverse=True):
238
- if cluster_type == "leiden":
239
- sc.tl.leiden(adata, random_state=0, resolution=resolution, key_added="tmp")
240
- else:
241
- sc.tl.louvain(adata, random_state=0, resolution=resolution, key_added="tmp")
242
- n_found = adata.obs["tmp"].nunique()
243
- if n_found == target_clusters:
244
- del adata.obs["tmp"]
245
- return float(resolution)
246
- del adata.obs["tmp"]
247
- raise RuntimeError(f"Could not find a {cluster_type} resolution for {target_clusters} clusters.")
248
-
249
-
250
- def run_graph_clustering(
251
- embedding: np.ndarray,
252
- cluster_type: str,
253
- target_clusters: int,
254
- ) -> tuple[np.ndarray, float]:
255
- adata_embedding = ad.AnnData(embedding)
256
- sc.tl.pca(adata_embedding, n_comps=min(50, embedding.shape[1]), svd_solver="arpack")
257
- sc.pp.neighbors(adata_embedding, n_neighbors=20, n_pcs=min(50, embedding.shape[1]))
258
- resolution = search_fixed_resolution(cluster_type, adata_embedding, target_clusters)
259
- if cluster_type == "leiden":
260
- sc.tl.leiden(adata_embedding, key_added="cluster", resolution=resolution)
261
- else:
262
- sc.tl.louvain(adata_embedding, key_added="cluster", resolution=resolution)
263
- labels = adata_embedding.obs["cluster"].astype(int).to_numpy()
264
- return labels, resolution
265
-
266
-
267
- def parse_args() -> argparse.Namespace:
268
- parser = argparse.ArgumentParser(description="Run CCST on a Visium dataset.")
269
- parser.add_argument("--visium-dir", type=Path, required=True, help="Path to the Visium sample directory.")
270
- parser.add_argument("--output-dir", type=Path, required=True, help="Directory for intermediate files and outputs.")
271
- parser.add_argument(
272
- "--count-file",
273
- default="filtered_feature_bc_matrix.h5",
274
- help="Count file inside the Visium directory.",
275
- )
276
- parser.add_argument(
277
- "--metadata-file",
278
- default="metadata.tsv",
279
- help="Metadata TSV inside the Visium directory.",
280
- )
281
- parser.add_argument(
282
- "--ground-truth-column",
283
- default="layer_guess",
284
- help="Column in the metadata TSV used as ground truth.",
285
- )
286
- parser.add_argument(
287
- "--profile",
288
- choices=["original", "optimized"],
289
- default="optimized",
290
- help="Preprocessing profile matching the local original or optimized script.",
291
- )
292
- parser.add_argument("--use-hvg", dest="use_hvg", action="store_true", default=None)
293
- parser.add_argument("--no-hvg", dest="use_hvg", action="store_false")
294
- parser.add_argument("--n-top-genes", type=int, default=None)
295
- parser.add_argument("--pca-n-comps", type=int, default=None)
296
- parser.add_argument("--min-cells", type=int, default=5)
297
- parser.add_argument("--distance-threshold", type=float, default=200.0)
298
- parser.add_argument("--lambda-i", type=float, default=0.3)
299
- parser.add_argument("--epochs", type=int, default=5000)
300
- parser.add_argument("--learning-rate", type=float, default=1e-6)
301
- parser.add_argument("--hidden-dim", type=int, default=256)
302
- parser.add_argument(
303
- "--cluster-type",
304
- choices=["kmeans", "leiden", "louvain"],
305
- default="kmeans",
306
- )
307
- parser.add_argument("--seed", type=int, default=0)
308
- return parser.parse_args()
309
-
310
-
311
- def main() -> None:
312
- args = parse_args()
313
- config = resolve_preprocessing(args)
314
- set_seed(args.seed)
315
- args.output_dir.mkdir(parents=True, exist_ok=True)
316
-
317
- tracemalloc.start()
318
- start_time = time.time()
319
-
320
- adata = sc.read_visium(
321
- str(args.visium_dir),
322
- count_file=args.count_file,
323
- load_images=True,
324
- )
325
- adata.var_names_make_unique()
326
-
327
- metadata = pd.read_csv(args.visium_dir / args.metadata_file, sep="\t")
328
- if not metadata.index.isin(adata.obs_names).all():
329
- first_column = metadata.columns[0]
330
- if metadata[first_column].astype(str).is_unique:
331
- metadata = metadata.set_index(first_column)
332
- missing_obs = [name for name in adata.obs_names if name not in metadata.index]
333
- if missing_obs:
334
- preview = ", ".join(missing_obs[:5])
335
- raise KeyError(
336
- f"Metadata file does not contain {len(missing_obs)} Visium barcodes. "
337
- f"First missing entries: {preview}"
338
- )
339
-
340
- adata.obs["ground_truth"] = metadata.loc[adata.obs_names, args.ground_truth_column]
341
- adata = adata[~pd.isnull(adata.obs["ground_truth"])].copy()
342
- n_clusters = int(adata.obs["ground_truth"].nunique())
343
-
344
- features, adata = preprocess_adata(adata, min_cells=args.min_cells, config=config)
345
- coordinates = np.asarray(adata.obsm["spatial"], dtype=np.float32)
346
- adjacency_raw = build_adjacency_matrix(coordinates, threshold=args.distance_threshold)
347
-
348
- np.save(args.output_dir / "features.npy", features)
349
- np.save(args.output_dir / "coordinates.npy", coordinates)
350
- with open(args.output_dir / "Adjacent", "wb") as handle:
351
- pickle.dump(adjacency_raw, handle)
352
- save_cell_types(adata.obs["ground_truth"].to_numpy(), args.output_dir)
353
-
354
- _, adjacency, features, _ = load_graph_inputs(args.output_dir, lambda_i=args.lambda_i)
355
- print(f"Adjacency shape: {adjacency.shape} | edges={adjacency.nnz}")
356
- print(f"Feature shape: {features.shape}")
357
-
358
- graphs = to_pyg_graph(adjacency, features)
359
- data_loader = DataLoader(graphs, batch_size=1)
360
- device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
361
- checkpoint_path = args.output_dir / f"dgi_lambdaI_{args.lambda_i}_epoch{args.epochs}.pth.tar"
362
- model = train_dgi(
363
- data_loader=data_loader,
364
- in_channels=features.shape[1],
365
- hidden_dim=args.hidden_dim,
366
- epochs=args.epochs,
367
- learning_rate=args.learning_rate,
368
- device=device,
369
- model_path=checkpoint_path,
370
- )
371
-
372
- model.eval()
373
- with torch.no_grad():
374
- batch = graphs[0].to(device)
375
- embedding, _, _ = model(batch)
376
- embedding_np = embedding.cpu().numpy()
377
-
378
- embedding_path = args.output_dir / f"lambdaI_{args.lambda_i}_epoch{args.epochs}_embed_x.npy"
379
- np.save(embedding_path, embedding_np)
380
-
381
- if args.cluster_type == "kmeans":
382
- labels, cluster_metric = run_kmeans(embedding_np, n_clusters=n_clusters, random_state=args.seed)
383
- cluster_metric_name = "silhouette"
384
- else:
385
- labels, cluster_metric = run_graph_clustering(
386
- embedding_np,
387
- cluster_type=args.cluster_type,
388
- target_clusters=n_clusters,
389
- )
390
- cluster_metric_name = "resolution"
391
-
392
- adata.obs["CCST"] = pd.Categorical(labels)
393
- ari = float(metrics.adjusted_rand_score(adata.obs["CCST"], adata.obs["ground_truth"]))
394
- results_path = args.output_dir / "CCST_results.h5ad"
395
- adata.write_h5ad(results_path)
396
-
397
- elapsed_seconds = time.time() - start_time
398
- _, peak_bytes = tracemalloc.get_traced_memory()
399
- tracemalloc.stop()
400
-
401
- summary = {
402
- "visium_dir": str(args.visium_dir),
403
- "output_dir": str(args.output_dir),
404
- "profile": args.profile,
405
- "use_hvg": config.use_hvg,
406
- "n_top_genes": config.n_top_genes,
407
- "pca_n_comps": config.pca_n_comps,
408
- "min_cells": args.min_cells,
409
- "distance_threshold": args.distance_threshold,
410
- "lambda_i": args.lambda_i,
411
- "epochs": args.epochs,
412
- "hidden_dim": args.hidden_dim,
413
- "cluster_type": args.cluster_type,
414
- "cluster_metric_name": cluster_metric_name,
415
- "cluster_metric_value": cluster_metric,
416
- "ari": ari,
417
- "n_clusters": n_clusters,
418
- "elapsed_seconds": elapsed_seconds,
419
- "peak_memory_mb": peak_bytes / 1024 / 1024,
420
- "checkpoint_path": str(checkpoint_path),
421
- "embedding_path": str(embedding_path),
422
- "results_path": str(results_path),
423
- }
424
-
425
- with open(args.output_dir / "run_summary.json", "w", encoding="utf-8") as handle:
426
- json.dump(summary, handle, indent=2)
427
-
428
- print(json.dumps(summary, indent=2))
429
-
430
-
431
- if __name__ == "__main__":
432
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
upload_to_hf.py CHANGED
@@ -1,55 +1,55 @@
1
- #!/usr/bin/env python
2
- """Create a private Hugging Face repo and upload this folder."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- from pathlib import Path
8
-
9
- from huggingface_hub import HfApi
10
-
11
-
12
- def parse_args() -> argparse.Namespace:
13
- parser = argparse.ArgumentParser(description="Upload this folder to Hugging Face.")
14
- parser.add_argument(
15
- "--repo-id",
16
- default="hutaobo/ccst-spatial-clustering",
17
- help="Full repo id, e.g. hutaobo/ccst-spatial-clustering",
18
- )
19
- parser.add_argument("--private", action="store_true", help="Create the repo as private.")
20
- parser.add_argument(
21
- "--repo-type",
22
- default="model",
23
- choices=["model", "dataset", "space"],
24
- help="Repository type on the Hugging Face Hub.",
25
- )
26
- parser.add_argument(
27
- "--folder-path",
28
- type=Path,
29
- default=Path(__file__).resolve().parent,
30
- help="Local folder to upload.",
31
- )
32
- parser.add_argument(
33
- "--commit-message",
34
- default="Upload CCST publication companion repository",
35
  help="Commit message used for the Hub upload.",
36
  )
37
  return parser.parse_args()
38
-
39
-
40
- def main() -> None:
41
- args = parse_args()
42
- api = HfApi()
43
- api.create_repo(repo_id=args.repo_id, private=args.private, exist_ok=True, repo_type=args.repo_type)
44
- api.upload_folder(
45
- folder_path=str(args.folder_path),
46
- repo_id=args.repo_id,
47
- repo_type=args.repo_type,
48
- commit_message=args.commit_message,
49
- ignore_patterns=["__pycache__", "*.pyc", "references/*.pdf"],
50
- )
51
- print(f"https://huggingface.co/{args.repo_id}")
52
-
53
-
54
- if __name__ == "__main__":
55
- main()
 
1
+ #!/usr/bin/env python
2
+ """Upload this repository to the Hugging Face Hub."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+
9
+ from huggingface_hub import HfApi
10
+
11
+
12
+ def parse_args() -> argparse.Namespace:
13
+ parser = argparse.ArgumentParser(description="Upload this repository to Hugging Face.")
14
+ parser.add_argument(
15
+ "--repo-id",
16
+ default="hutaobo/ccst-spatial-clustering",
17
+ help="Full repo id, e.g. hutaobo/ccst-spatial-clustering",
18
+ )
19
+ parser.add_argument("--private", action="store_true", help="Create the repo as private.")
20
+ parser.add_argument(
21
+ "--repo-type",
22
+ default="model",
23
+ choices=["model", "dataset", "space"],
24
+ help="Repository type on the Hugging Face Hub.",
25
+ )
26
+ parser.add_argument(
27
+ "--folder-path",
28
+ type=Path,
29
+ default=Path(__file__).resolve().parent,
30
+ help="Local folder to upload.",
31
+ )
32
+ parser.add_argument(
33
+ "--commit-message",
34
+ default="Update manuscript companion placeholder",
35
  help="Commit message used for the Hub upload.",
36
  )
37
  return parser.parse_args()
38
+
39
+
40
+ def main() -> None:
41
+ args = parse_args()
42
+ api = HfApi()
43
+ api.create_repo(repo_id=args.repo_id, private=args.private, exist_ok=True, repo_type=args.repo_type)
44
+ api.upload_folder(
45
+ folder_path=str(args.folder_path),
46
+ repo_id=args.repo_id,
47
+ repo_type=args.repo_type,
48
+ commit_message=args.commit_message,
49
+ ignore_patterns=["__pycache__", "*.pyc", "references/*.pdf"],
50
+ )
51
+ print(f"https://huggingface.co/{args.repo_id}")
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()