Skip to content

Commit cfabe1b

Browse files
committed
Add all pipeline files
1 parent 85c264e commit cfabe1b

19 files changed

Lines changed: 1721 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI — Lint + Demo
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
lint-and-demo:
11+
name: Python lint + demo run
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.10"
21+
22+
- name: Install dependencies
23+
run: |
24+
pip install --upgrade pip
25+
pip install scanpy squidpy anndata numpy pandas scipy \
26+
matplotlib seaborn pyyaml flake8
27+
28+
- name: Lint with flake8
29+
run: |
30+
flake8 pipeline/ scripts/ run_pipeline.py \
31+
--max-line-length=100 \
32+
--ignore=E501,W503 \
33+
--exclude=__init__.py
34+
continue-on-error: true
35+
36+
- name: Run demo pipeline
37+
run: python scripts/run_demo.py
38+
39+
- name: Check outputs exist
40+
run: |
41+
ls results/demo/qc/
42+
ls results/demo/spatial_de/

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
data/
2+
results/
3+
*.h5ad
4+
*.h5
5+
__pycache__/
6+
*.pyc
7+
.ipynb_checkpoints/
8+
*.egg-info/
9+
.DS_Store
10+
Thumbs.db
11+
.Rhistory
12+
*.RData

R/run_rctd.R

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env Rscript
2+
# R/run_rctd.R
3+
# RCTD cell type deconvolution for 10x Visium
4+
# Called from Python via rpy2 or standalone
5+
# Author: Shivani Patel
6+
7+
suppressPackageStartupMessages({
8+
library(spacexr) # RCTD
9+
library(Matrix)
10+
library(dplyr)
11+
library(ggplot2)
12+
})
13+
14+
15+
run_rctd_pipeline <- function(spaceranger_path, reference_path,
16+
sample_id, outdir) {
17+
dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
18+
19+
# ── Load Visium spatial data ───────────────────────────────────────────────
20+
counts <- load_visium_counts(spaceranger_path)
21+
coords <- load_visium_coords(spaceranger_path)
22+
23+
# ── Build SpatialRNA object ────────────────────────────────────────────────
24+
puck <- SpatialRNA(coords, counts, require.int = FALSE)
25+
26+
# ── Load single-cell reference ─────────────────────────────────────────────
27+
reference <- load_sc_reference(reference_path)
28+
29+
# ── Run RCTD ──────────────────────────────────────────────────────────────
30+
message(" Running RCTD deconvolution...")
31+
myRCTD <- create.RCTD(puck, reference, max_cores = 4)
32+
myRCTD <- run.RCTD(myRCTD, doublet_mode = "full")
33+
34+
# ── Extract proportions ────────────────────────────────────────────────────
35+
results <- myRCTD@results
36+
norm_weights <- normalize_weights(results$weights)
37+
proportions <- as.data.frame(as.matrix(norm_weights))
38+
39+
# ── Save ──────────────────────────────────────────────────────────────────
40+
write.table(proportions,
41+
file.path(outdir, paste0(sample_id, "_rctd_proportions.tsv")),
42+
sep = "\t", quote = FALSE)
43+
44+
# ── Plot spatial proportions ───────────────────────────────────────────────
45+
plot_rctd_spatial(proportions, coords, sample_id, outdir)
46+
47+
return(proportions)
48+
}
49+
50+
51+
load_visium_counts <- function(path) {
52+
h5_file <- file.path(path, "filtered_feature_bc_matrix.h5")
53+
if (file.exists(h5_file)) {
54+
return(Seurat::Read10X_h5(h5_file))
55+
}
56+
# Try MEX format
57+
mex_path <- file.path(path, "filtered_feature_bc_matrix")
58+
if (dir.exists(mex_path)) {
59+
return(Seurat::Read10X(mex_path))
60+
}
61+
stop("Cannot find count matrix in: ", path)
62+
}
63+
64+
65+
load_visium_coords <- function(path) {
66+
coords_file <- file.path(path, "spatial", "tissue_positions_list.csv")
67+
if (!file.exists(coords_file)) {
68+
coords_file <- file.path(path, "spatial", "tissue_positions.csv")
69+
}
70+
coords <- read.csv(coords_file, header = FALSE)
71+
colnames(coords) <- c("barcode", "in_tissue", "row", "col", "y", "x")
72+
coords <- coords[coords$in_tissue == 1, ]
73+
rownames(coords) <- coords$barcode
74+
return(coords[, c("x", "y")])
75+
}
76+
77+
78+
load_sc_reference <- function(ref_path) {
79+
if (!file.exists(ref_path)) {
80+
message(" Reference not found — using demo reference")
81+
return(make_demo_reference())
82+
}
83+
# Load h5ad via reticulate/SeuratDisk
84+
tryCatch({
85+
library(SeuratDisk)
86+
Convert(ref_path, dest = "h5seurat", overwrite = TRUE)
87+
seurat_obj <- LoadH5Seurat(gsub("\\.h5ad$", ".h5seurat", ref_path))
88+
cell_types <- as.factor(seurat_obj$cell_type)
89+
counts <- seurat_obj@assays$RNA@counts
90+
return(Reference(counts, cell_types))
91+
}, error = function(e) {
92+
message(" Reference load failed: ", e$message, " — using demo reference")
93+
return(make_demo_reference())
94+
})
95+
}
96+
97+
98+
make_demo_reference <- function() {
99+
set.seed(42)
100+
n_cells <- 500
101+
n_genes <- 2000
102+
cell_types <- factor(sample(
103+
c("Tumor", "T_cell_CD8", "T_cell_CD4", "Macrophage_M1",
104+
"Macrophage_M2", "CAF", "Endothelial", "B_cell"),
105+
n_cells, replace = TRUE
106+
))
107+
counts <- matrix(
108+
rnbinom(n_cells * n_genes, mu = 2, size = 0.5),
109+
nrow = n_genes, ncol = n_cells,
110+
dimnames = list(paste0("GENE_", seq_len(n_genes)),
111+
paste0("CELL_", seq_len(n_cells)))
112+
)
113+
Reference(Matrix::Matrix(counts, sparse = TRUE), cell_types)
114+
}
115+
116+
117+
plot_rctd_spatial <- function(proportions, coords, sample_id, outdir) {
118+
cell_types <- colnames(proportions)
119+
shared <- intersect(rownames(proportions), rownames(coords))
120+
if (length(shared) == 0) return(invisible(NULL))
121+
122+
plot_df <- cbind(
123+
coords[shared, ],
124+
proportions[shared, ]
125+
)
126+
127+
pdf(file.path(outdir, paste0(sample_id, "_rctd_spatial.pdf")),
128+
width = 4 * min(4, length(cell_types)),
129+
height = 4 * ceiling(length(cell_types) / 4))
130+
131+
par(mfrow = c(ceiling(length(cell_types) / 4), min(4, length(cell_types))),
132+
mar = c(2, 2, 2, 2))
133+
134+
for (ct in cell_types) {
135+
vals <- plot_df[[ct]]
136+
col_scale <- colorRampPalette(c("#f8f9fa", "#e76f51"))(100)
137+
breaks <- seq(0, max(vals, na.rm = TRUE), length.out = 101)
138+
col_idx <- findInterval(vals, breaks, rightmost.closed = TRUE)
139+
140+
plot(plot_df$x, plot_df$y,
141+
col = col_scale[pmax(1, pmin(100, col_idx))],
142+
pch = 16, cex = 0.5,
143+
main = gsub("_", " ", ct),
144+
xlab = "", ylab = "", axes = FALSE)
145+
}
146+
dev.off()
147+
}

config/config.yaml

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# config/config.yaml
2+
# Spatial Transcriptomics Pipeline — 10x Visium
3+
4+
# ── Samples ───────────────────────────────────────────────────────────────────
5+
samples:
6+
- id: "SAMPLE_01"
7+
spaceranger: "data/spaceranger/SAMPLE_01"
8+
condition: "tumor"
9+
patient: "P01"
10+
tissue: "HNSCC"
11+
12+
- id: "SAMPLE_02"
13+
spaceranger: "data/spaceranger/SAMPLE_02"
14+
condition: "adjacent_normal"
15+
patient: "P01"
16+
tissue: "HNSCC"
17+
18+
- id: "SAMPLE_03"
19+
spaceranger: "data/spaceranger/SAMPLE_03"
20+
condition: "tumor"
21+
patient: "P02"
22+
tissue: "HNSCC"
23+
24+
# ── QC thresholds ─────────────────────────────────────────────────────────────
25+
qc:
26+
min_counts: 500 # Min UMI counts per spot
27+
max_counts: 50000 # Max UMI (cap doublet-like spots)
28+
min_genes: 200 # Min genes per spot
29+
max_pct_mito: 25 # Max % mitochondrial counts
30+
min_spots_per_gene: 3 # Min spots expressing a gene
31+
flag_tissue_only: true # Restrict to tissue-covered spots
32+
33+
# ── Preprocessing ─────────────────────────────────────────────────────────────
34+
preprocessing:
35+
normalization: "scran" # "scran" | "scanpy_normalize_total"
36+
log_transform: true
37+
n_top_genes: 3000 # Highly variable genes
38+
scale: true
39+
regress_out:
40+
- "total_counts"
41+
- "pct_counts_mt"
42+
43+
# ── Dimensionality reduction ──────────────────────────────────────────────────
44+
dim_reduction:
45+
n_pcs: 50
46+
n_neighbors: 15
47+
umap_min_dist: 0.3
48+
49+
# ── Spatially-aware clustering ────────────────────────────────────────────────
50+
clustering:
51+
method: "leiden" # "leiden" | "louvain" | "mclust"
52+
resolution: [0.3, 0.5, 0.8] # Multiple resolutions evaluated
53+
use_spatial: true # Weight graph by spatial proximity
54+
spatial_key: "spatial"
55+
56+
# ── SpatialDE ────────────────────────────────────────────────────────────────
57+
spatial_de:
58+
n_top_svg: 500 # Top spatially variable genes
59+
omnibus: true # Run SpatialDE omnibus test
60+
pattern_discovery: true # Automatic expression pattern discovery
61+
62+
# ── Cell type deconvolution ───────────────────────────────────────────────────
63+
deconvolution:
64+
method: "RCTD" # "RCTD" | "STdeconvolve" | "cell2location"
65+
reference: "data/reference/sc_reference.h5ad" # scRNA-seq reference
66+
cell_types:
67+
- "Tumor"
68+
- "T_cell_CD8"
69+
- "T_cell_CD4"
70+
- "Macrophage_M1"
71+
- "Macrophage_M2"
72+
- "CAF" # Cancer-associated fibroblast
73+
- "Endothelial"
74+
- "B_cell"
75+
- "NK_cell"
76+
- "Dendritic_cell"
77+
78+
# ── Tumor microenvironment ────────────────────────────────────────────────────
79+
tme:
80+
tumor_label: "Tumor"
81+
immune_labels:
82+
- "T_cell_CD8"
83+
- "T_cell_CD4"
84+
- "Macrophage_M1"
85+
- "Macrophage_M2"
86+
- "NK_cell"
87+
stromal_labels:
88+
- "CAF"
89+
- "Endothelial"
90+
neighborhood_radius: 150 # µm radius for neighborhood analysis
91+
92+
# ── Cell-cell communication ────────────────────────────────────────────────────
93+
communication:
94+
method: "squidpy" # "squidpy" | "cellchat_r"
95+
library: "CellChatDB"
96+
n_permutations: 1000
97+
coord_type: "generic"
98+
n_rings: 2 # Spatial neighbor rings
99+
100+
# ── Differential expression ───────────────────────────────────────────────────
101+
differential_expression:
102+
method: "wilcoxon"
103+
alpha: 0.05
104+
lfc_min: 0.5
105+
min_pct: 0.1
106+
compare:
107+
- ["tumor", "adjacent_normal"]
108+
109+
# ── Visualization ─────────────────────────────────────────────────────────────
110+
visualization:
111+
dpi: 300
112+
figsize: [10, 8]
113+
colormap: "viridis"
114+
spot_size: 1.5
115+
save_formats: ["pdf", "png"]
116+
117+
# ── Output ────────────────────────────────────────────────────────────────────
118+
output:
119+
save_h5ad: true
120+
save_figures: true
121+
report: true

envs/environment.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: spatial-tx
2+
channels:
3+
- conda-forge
4+
- bioconda
5+
- defaults
6+
dependencies:
7+
- python=3.10
8+
- pip
9+
10+
# Core scientific
11+
- numpy>=1.24
12+
- pandas>=2.0
13+
- scipy>=1.10
14+
- scikit-learn>=1.3
15+
16+
# Spatial transcriptomics
17+
- scanpy>=1.9
18+
- squidpy>=1.3
19+
- anndata>=0.9
20+
21+
# Visualization
22+
- matplotlib>=3.7
23+
- seaborn>=0.12
24+
25+
# Utilities
26+
- pyyaml>=6.0
27+
- tqdm
28+
- joblib
29+
30+
- pip:
31+
- SpatialDE>=1.1.3
32+
- cell2location>=0.1.3
33+
- rpy2>=3.5 # R bridge for RCTD/STdeconvolve
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)