#!/usr/bin/env python
"""公开数据的真实空间转录组分析样例(10x Visium,人淋巴结)。

空间是单价最高的一档服务($550–1,400/张切片),所以样品必须是真跑的:
质控 → 过滤 → 标准化 → 聚类 → 空间域可视化 → 空间自相关(Moran's I)找空间变异基因。
所有数字与图都由本脚本算出,可复算。
"""
import json, os, warnings
warnings.filterwarnings("ignore")
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import scanpy as sc
import numpy as np

OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "out_spatial")
os.makedirs(OUT, exist_ok=True)
sc.settings.figdir = OUT
sc.settings.set_figure_params(dpi=140, frameon=False, facecolor="white")
M = {}

adata = sc.datasets.visium_sge(sample_id="V1_Human_Lymph_Node")
adata.var_names_make_unique()
M["spots_raw"], M["genes_raw"] = int(adata.n_obs), int(adata.n_vars)

adata.var["mt"] = adata.var_names.str.startswith("MT-")
sc.pp.calculate_qc_metrics(adata, qc_vars=["mt"], inplace=True, percent_top=None, log1p=False)
M["median_genes_per_spot"] = float(np.median(adata.obs["n_genes_by_counts"]))
M["median_umi_per_spot"] = float(np.median(adata.obs["total_counts"]))
M["median_pct_mito"] = round(float(np.median(adata.obs["pct_counts_mt"])), 2)

fig, axes = plt.subplots(1, 3, figsize=(12, 3.4))
for ax, key, title in zip(axes, ["total_counts", "n_genes_by_counts", "pct_counts_mt"],
                          ["UMIs per spot", "Genes per spot", "Mitochondrial %"]):
    ax.hist(adata.obs[key].values, bins=60, color="#0a6ed1")
    ax.set_title(title, fontsize=11); ax.set_yticks([])
plt.tight_layout(); plt.savefig(f"{OUT}/spatial_qc.png", dpi=140, bbox_inches="tight"); plt.close()

sc.pp.filter_cells(adata, min_counts=500)
sc.pp.filter_genes(adata, min_cells=10)
M["spots_after_qc"], M["genes_after_qc"] = int(adata.n_obs), int(adata.n_vars)
M["spots_removed_pct"] = round(100 * (1 - M["spots_after_qc"] / M["spots_raw"]), 1)

sc.pp.normalize_total(adata, inplace=True)
sc.pp.log1p(adata)
sc.pp.highly_variable_genes(adata, flavor="seurat", n_top_genes=2000)
M["hvg"] = int(adata.var.highly_variable.sum())
sc.pp.pca(adata, n_comps=50)
sc.pp.neighbors(adata)
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=1.0, key_added="domain", flavor="igraph", n_iterations=2, directed=False)
M["domains"] = int(adata.obs["domain"].nunique())
M["domain_sizes"] = {k: int(v) for k, v in adata.obs["domain"].value_counts().items()}

sc.pl.spatial(adata, color="domain", size=1.4, title="Spatial domains (Leiden)",
              save="_domains.png", show=False)
sc.pl.spatial(adata, color="total_counts", size=1.4, title="UMIs per spot",
              save="_counts.png", show=False)

# 空间自相关:Moran's I 找真正随位置变化的基因
sc.pp.neighbors(adata, key_added="spatial_neigh", use_rep="X_pca")
hvg = adata.var_names[adata.var.highly_variable][:1500]
mi = sc.metrics.morans_i(adata[:, hvg])
order = np.argsort(mi)[::-1]
top_sv = [(str(hvg[i]), round(float(mi[i]), 3)) for i in order[:15]]
M["top_spatially_variable"] = top_sv

sc.tl.rank_genes_groups(adata, "domain", method="wilcoxon")
M["domain_markers"] = {g: [str(x) for x in adata.uns["rank_genes_groups"]["names"][g][:6]]
                       for g in adata.obs["domain"].cat.categories}

sc.pl.spatial(adata, color=[t[0] for t in top_sv[:4]], size=1.4, ncols=4,
              save="_svg.png", show=False)

with open(f"{OUT}/metrics.json", "w") as f:
    json.dump(M, f, indent=2, ensure_ascii=False)
print(json.dumps({k: v for k, v in M.items() if k not in ("domain_markers", "domain_sizes")}, indent=2))
print("输出:", OUT)
