Correct public repo scope and remove legacy CCST files
Browse files- .gitattributes +35 -35
- .gitignore +4 -4
- CITATION.cff +16 -16
- LICENSE.md +7 -7
- MERGE_NOTES.md +0 -50
- README.md +28 -111
- references/REFERENCE_NOTE.md +0 -5
- requirements.txt +0 -10
- run_ccst.py +0 -432
- 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: "
|
| 3 |
-
title: "
|
| 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
|
| 14 |
-
-
|
| 15 |
-
-
|
| 16 |
-
|
| 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: "
|
|
|
|
| 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 |
-
|
| 2 |
Non-Commercial Distribution Notice
|
| 3 |
|
| 4 |
-
|
| 5 |
|
| 6 |
-
|
| 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
|
| 21 |
-
- Appropriate attribution should be given when redistributing or adapting
|
| 22 |
- Commercial use is not permitted under this repository-level release setting.
|
| 23 |
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
|
|
|
| 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 |
-
#
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
-
|
| 17 |
-
- `CCST_Sample_Optimized.py`
|
| 18 |
|
| 19 |
-
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
This repository is
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 71 |
|
| 72 |
-
``
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
--profile optimized
|
| 77 |
-
```
|
| 78 |
|
| 79 |
-
|
| 80 |
|
| 81 |
-
|
| 82 |
-
python run_ccst.py \
|
| 83 |
-
--visium-dir ./DLPFC/151673 \
|
| 84 |
-
--output-dir ./outputs/151673_original \
|
| 85 |
-
--profile original
|
| 86 |
-
```
|
| 87 |
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 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 |
-
|
| 135 |
-
|
| 136 |
-
The repository includes a `CITATION.cff` file to support software citation workflows.
|
| 137 |
|
| 138 |
-
##
|
| 139 |
|
| 140 |
-
|
| 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 |
-
"""
|
| 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
|
| 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="
|
| 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()
|