Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e54d99f
Add archetype projection tool to sc.tl
SupaNautilus Jul 29, 2026
6304e1b
Remove empty archetype test file
SupaNautilus Jul 31, 2026
b798055
Refactor archetype tests: portable paths, locked seed, baseline valid…
SupaNautilus Aug 14, 2026
a66708c
Add generate_pr_plots.py script for archetype visualization
SupaNautilus Aug 14, 2026
7deb1fd
Fix Ruff EM102 lint errors and update archetype tool
SupaNautilus Aug 19, 2026
56c0243
Refactor comments and baseline generation logic
SupaNautilus Sep 7, 2026
fafaaac
Add tumor_sub_corey_clean.h5ad test dataset asset
SupaNautilus Sep 17, 2026
7e6e5c0
test(archetype): update baseline expected_archetypes.npy for full tum…
SupaNautilus Sep 17, 2026
b6284b9
Merge branch 'scverse:main' into add-archetype-projection
SupaNautilus Oct 5, 2026
bd90c79
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 10, 2026
c217b9b
chore: credit project contributors and advisors
SupaNautilus Oct 10, 2026
ed1f1d5
Merge branch 'main' into add-archetype-projection
SupaNautilus Oct 10, 2026
4ee200a
fix: use scanpy.io import in archetype tests
SupaNautilus Oct 17, 2026
16367e7
Merge branch 'main' into add-archetype-projection
SupaNautilus Oct 17, 2026
2950a6f
Merge branch 'main' into add-archetype-projection
SupaNautilus Oct 23, 2026
21f016d
Merge branch 'main' into add-archetype-projection
SupaNautilus Oct 27, 2026
a84071f
Merge branch 'scverse:main' into add-archetype-projection
SupaNautilus Oct 29, 2026
33b8c1a
Merge branch 'main' into add-archetype-projection
SupaNautilus Nov 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ Thumbs.db
# asv benchmark files
/benchmarks/.asv
/benchmarks/data/

tests/_data/*.h5ad
output*.png
85 changes: 85 additions & 0 deletions generate_pr_plots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Generate PR validation plots for archetype projection."""

from __future__ import annotations

import sys
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

import scanpy as sc

# Add repository root to Python path
sys.path.insert(0, str(Path(__file__).parent))

from tests.test_archetype import DATA_PATH, load_model

import scanpy.tools._archetype as arch_mod

# -------------------------------------------------------------------------
# 1. Load Data & Run Archetype Pipeline
# -------------------------------------------------------------------------
print("[INFO] Loading AnnData dataset...")
adata = sc.read_h5ad(DATA_PATH)

print("[INFO] Loading model weights and scaling factors...")
w, s, _ = load_model(adata)

print("[INFO] Computing archetype probabilities...")
np.random.seed(42) # noqa: NPY002
arch_mod.archetype(adata, w=w, s=s)

# -------------------------------------------------------------------------
# 2. Compute Hexagon Coordinates directly from Archetype Matrix
# -------------------------------------------------------------------------
print("[INFO] Computing hexagon coordinates from archetype matrix...")
arch_matrix = adata.obsm["archetypes"]
res = arch_mod.compute_hexagon_coordinates(arch_matrix)

# Handle tuple return (x_coords, y_coords)
coords = np.column_stack((res[0], res[1])) if isinstance(res, tuple) else res

adata.obsm["X_archetype_hexagon"] = coords

# -------------------------------------------------------------------------
# 3. Extract Archetype Scores & Render Plot
# -------------------------------------------------------------------------
archetype_names = [
"Survival",
"Proliferation",
"Fibroblastic",
"Energy",
"Biomass",
"Senescence",
]
for i, name in enumerate(archetype_names):
adata.obs[name] = adata.obsm["archetypes"][:, i]

print("[INFO] Rendering hexagon spatial projection plot...")
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.flatten()

for i, name in enumerate(archetype_names):
ax = axes[i]
scatter = ax.scatter(
coords[:, 0],
coords[:, 1],
c=adata.obs[name],
cmap="magma",
s=3,
alpha=0.7,
)
ax.set_title(f"Archetype: {name}", fontsize=12, fontweight="bold")
ax.set_aspect("equal")
ax.axis("off")
fig.colorbar(scatter, ax=ax, fraction=0.046, pad=0.04)

plt.suptitle("Hexagon Spatial Projection of Tumor Archetypes", fontsize=16)
plt.tight_layout()

output_path = Path("archetype_hexagon_plot.png")
plt.savefig(output_path, dpi=300, bbox_inches="tight")
plt.close()

print(f"[SUCCESS] Visual proof plot saved successfully to: {output_path.resolve()}")
8 changes: 8 additions & 0 deletions src/scanpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,11 @@ def __getattr__(name: str) -> Any:
return read_visium # warns on call already

raise AttributeError


# Add archetype to imports in src/scanpy/tools/__init__.py
from scanpy.tools._archetype import archetype

__all__ = [
"archetype",
]
2 changes: 2 additions & 0 deletions src/scanpy/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from typing import TYPE_CHECKING

from ._archetype import archetype
from ._dendrogram import dendrogram
from ._diffmap import diffmap
from ._dpt import dpt
Expand Down Expand Up @@ -41,6 +42,7 @@ def __getattr__(name: str) -> Any:


__all__ = [
"archetype",
"dendrogram",
"diffmap",
"dpt",
Expand Down
199 changes: 199 additions & 0 deletions src/scanpy/tools/_archetype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

import numpy as np
from scipy.sparse import csr_matrix # noqa: TID251

if TYPE_CHECKING:
from anndata import AnnData

_PACKAGE_DATA = Path(__file__).parent / "_data"


def archetype(
adata: AnnData,
w=None,
s=None,
):
"""Compute archetype scores and store them in adata.obsm["archetypes"]."""
archs = archetype_computer(
adata.X.transpose(),
w=w,
s=s,
)

if hasattr(archs, "toarray"):
archs = archs.toarray()

adata.obsm["archetypes"] = archs.T


def archetype_computer(
data,
w=None,
s=None,
):
"""
Compute archetype scores from expression data.

Parameters
----------
data
Gene expression matrix.
w
Pretrained archetype weight matrix.
s
Gene scaling factors.

Returns
-------
h
Archetype score matrix.
"""
# Load pretrained archetype weights if not supplied.
if w is None:
default_w_path = _PACKAGE_DATA / "Archetypes.csv"
if not default_w_path.exists():
msg = (
"Archetype matrix `w` was not provided and default "
f"file not found at {default_w_path}"
)
raise ValueError(msg)
w = csr_matrix(np.loadtxt(default_w_path, delimiter=","))

# Load pretrained gene scaling factors if not supplied.
if s is None:
default_s_path = _PACKAGE_DATA / "S.csv"
if not default_s_path.exists():
msg = (
"Scaling vector `s` was not provided and default "
f"file not found at {default_s_path}"
)
raise ValueError(msg)
s = np.loadtxt(default_s_path, delimiter=",")

# Reweight genes.
dd = data / s[:, None]

# Normalize each observation.
dd = dd / dd.sum(axis=0)

return nmf_new_weights(
dd,
w,
6,
)


def nmf_new_weights(
v,
w,
k,
):
"""
Compute archetype scores using pretrained weights.

Parameters
----------
v
Input expression matrix.
w
Pretrained archetype weight matrix.
k
Number of archetypes.

Returns
-------
h
Archetype score matrix.
"""
n_iter = 1000

# Fixed random seed for reproducibility.
rng = np.random.default_rng(0)

eps = np.finfo(float).eps

_, n = v.shape

# Initialize dense archetype matrix.
h = rng.random((k, n)) + eps

# Pre-calculate W^T * V outside the loop for speed
wv = w.T.dot(v)
if hasattr(wv, "toarray"):
wv = wv.toarray()

# Pre-calculate W^T * W outside the loop
w_tw = w.T.dot(w)
if hasattr(w_tw, "toarray"):
w_tw = w_tw.toarray()

# Multiplicative NMF update while keeping W fixed.
for _ in range(n_iter):
wwh = w_tw.dot(h) + eps
h *= wv / wwh

# Normalize archetype contributions per cell.
h_sum = h.sum(axis=0, keepdims=True)
h_sum[h_sum == 0] = 1.0
h /= h_sum

return h


def add_archetypes_to_adata(
adata: AnnData,
w=None,
s=None,
):
"""
Add archetype scores to an AnnData object.

Stores results in adata.obsm["archetypes"].
"""
archs = archetype_computer(
adata.X.transpose(),
w=w,
s=s,
)

if hasattr(archs, "toarray"):
archs = archs.toarray()

adata.obsm["archetypes"] = archs.T


def compute_hexagon_coordinates(
archetypes,
):
"""Convert archetype scores into two-dimensional coordinates."""
# Order archetypes according to the hexagon visualization layout.
hh = archetypes[:, [1, 2, 0, 4, 3, 5]]

directions = np.array([0, 1, 2, 3, 4, 5])

direction_x = np.cos(np.pi / 2 - 2 * np.pi * directions / 6)

direction_y = np.sin(np.pi / 2 - 2 * np.pi * directions / 6)

coor1 = hh.dot(direction_x)
coor2 = hh.dot(direction_y)

return coor1, coor2


def add_coor_to_adata(
adata: AnnData,
):
"""
Add hexagon coordinates to an AnnData object.

Stores coordinates in adata.obsm["Coor1"] and adata.obsm["Coor2"].
"""
coor1, coor2 = compute_hexagon_coordinates(adata.obsm["archetypes"])

adata.obsm["Coor1"] = coor1
adata.obsm["Coor2"] = coor2
Loading
Loading