deepspot-m: document the ~19k-gene panel limit, drop redundant eval calls, add diagram

- State that predict_genes only accepts symbols in the released tokens.csv
  panel (model.gene_names) and raises KeyError otherwise; temper the
  transcriptome-coverage claims to match.
- Use from_pretrained's device argument; the model already returns in eval
  mode and predict_genes runs under no_grad, so drop the redundant
  model.eval()/no_grad lines (also clears MDBLOCK_PYTHON_EVAL_EXEC scan FPs).
- Rebuild the tile batch inside the multi-source loop, matching the advice
  beneath the example.
- Pin the install to deepspotm==1.0.0.
- Add the required docs/images/deepspot-m.png workflow diagram.
This commit is contained in:
Timothy Kassis
2026-08-07 13:09:04 -07:00
parent de981da0ec
commit 947764127b
4 changed files with 41 additions and 23 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+11 -6
View File
@@ -21,8 +21,10 @@ A LoRA-adapted pathology foundation backbone (Midnight) tokenises the tile. A
cross-attention gene decoder lets each gene query attend to the patch tokens, and a gene
router hypernetwork builds gene-specific projections from frozen biological embeddings
(Evo 2, Orthrus, ProtT5, scGPT, Apertus). Genes enter the model as queryable embeddings
rather than fixed output slots, so coverage spans the protein-coding transcriptome
including genes unseen in training.
rather than fixed output slots, so the released model covers a ~19k protein-coding gene
panel including genes unseen in training. The panel ships with the weights as
`tokens.csv` and is exposed as `model.gene_names`; genes outside it cannot be queried in
this release.
Applied to TCGA, the model produced a virtual spatial transcriptomics atlas of 28,664
slides across 32 cancer types.
@@ -35,7 +37,7 @@ noncommercial research and check both licences before redistributing outputs.
## Installation
```bash
uv pip install deepspotm
uv pip install deepspotm==1.0.0
```
Version 1.0.0 targets Python 3.10 to 3.13 and pulls in PyTorch. Install the PyTorch build
@@ -67,7 +69,9 @@ vals = model.predict_genes(image_processor(pil_tile).unsqueeze(0), ["EPCAM", "CD
`pil_tile` is a PIL image of exactly 224x224 pixels. `image_processor` turns it into a
tensor, `unsqueeze(0)` adds the batch dimension, and `predict_genes` takes the batch plus a
list of HGNC gene symbols. Values come back in log1p-CPM, aligned with the gene list you
passed, so keep that list beside the output to keep the columns labelled.
passed, so keep that list beside the output to keep the columns labelled. Symbols must be
in the released ~19k-gene panel (`model.gene_names`); an unknown symbol raises `KeyError`
naming the offending genes.
## Tile requirements
@@ -100,7 +104,7 @@ an `ImportError` into a message that names every step:
```python
DEEPSPOTM_HELP = (
"DeepSpot-M is unavailable. Install it with `uv pip install deepspotm`, request "
"DeepSpot-M is unavailable. Install it with `uv pip install deepspotm==1.0.0`, request "
"access to the gated weights at https://huggingface.co/ratschlab/DeepSpotM, then "
"authenticate with `huggingface-cli login`."
)
@@ -150,7 +154,8 @@ worked loop, batch sizing and an `AnnData` assembly step.
- Spatial expression maps for marker genes across a tumour section.
- Transcriptome-wide prediction over a slide cohort with no matching assay run.
- Querying genes outside any fixed spatial panel, including genes unseen in training.
- Querying any of the ~19k panel genes by symbol, including genes unseen in training
far beyond the few hundred genes of a typical spatial assay panel.
- Adding an expression channel to a morphology-only histology pipeline.
- Building a slide-level cohort atlas, as done for TCGA.
+23 -9
View File
@@ -48,12 +48,12 @@ values stay comparable. When the choice matters to a conclusion, run the same ti
through several sources and report the values side by side:
```python
tiles = torch.stack([image_processor(require_tile(t)) for t in pil_tiles])
genes = ["EPCAM", "CD3D", "PTPRC"]
per_source = {}
for source in ("scgpt", "prott5", "evo2"):
model, image_processor = DeepSpotM.from_pretrained("ratschlab/DeepSpotM", source=source)
tiles = torch.stack([image_processor(require_tile(t)) for t in pil_tiles])
per_source[source] = model.predict_genes(tiles, genes)
```
@@ -72,7 +72,20 @@ symbols. A single tile still needs the batch dimension, which is what `unsqueeze
### Gene symbols
Pass HGNC gene symbols as uppercase strings, for example `EPCAM`, `CD3D`, `PTPRC`,
`MKI67`. Two habits keep a run reproducible:
`MKI67`. The queryable genes are the ~19k-symbol panel shipped with the weights as
`tokens.csv`, exposed on the loaded model as `model.gene_names`. A symbol outside that
panel raises `KeyError` naming the offending genes, and predicting genes outside the
panel is not part of this release. Check membership up front when a gene list comes from
elsewhere:
```python
panel = set(model.gene_names)
missing = [g for g in genes if g not in panel]
if missing:
raise ValueError(f"Not in the DeepSpot-M panel: {missing}")
```
Two habits keep a run reproducible:
- Map aliases to current HGNC symbols before querying, so `CD45` becomes `PTPRC`. Reading
the list from a file keeps the mapping visible in the run.
@@ -104,18 +117,19 @@ one call, so lower one when the other is large.
## Device placement
DeepSpot-M is a PyTorch model, so the usual handling applies. Move the model once, put
each batch on the same device, and predict under `torch.no_grad()`:
`from_pretrained` accepts a `device` argument and returns the model already in eval mode
on that device, and `predict_genes` runs under `no_grad` on its own. So device handling
is one argument plus putting each batch on the same device:
```python
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
model = model.to(device)
model.eval()
model, image_processor = DeepSpotM.from_pretrained(
"ratschlab/DeepSpotM", source="scgpt", device=device
)
with torch.no_grad():
vals = model.predict_genes(batch.to(device), genes)
vals = model.predict_genes(batch.to(device), genes)
```
Keeping the model on the device across batches is what makes a slide-scale run practical.
@@ -145,7 +159,7 @@ still pending. Report the whole path back to a working call rather than the raw
```python
DEEPSPOTM_HELP = (
"DeepSpot-M is unavailable. Install it with `uv pip install deepspotm`, request "
"DeepSpot-M is unavailable. Install it with `uv pip install deepspotm==1.0.0`, request "
"access to the gated weights at https://huggingface.co/ratschlab/DeepSpotM, then "
"authenticate with `huggingface-cli login`."
)
+7 -8
View File
@@ -76,20 +76,19 @@ def batched(items, size):
for start in range(0, len(items), size):
yield items[start : start + size]
model, image_processor = DeepSpotM.from_pretrained("ratschlab/DeepSpotM", source="scgpt")
device = "cuda" if torch.cuda.is_available() else "cpu"
model = model.to(device)
model.eval()
model, image_processor = DeepSpotM.from_pretrained(
"ratschlab/DeepSpotM", source="scgpt", device=device
)
genes = ["EPCAM", "CD3D", "PTPRC", "MKI67"]
tile_paths = sorted(Path("tiles/").glob("*.png"))
chunks = []
with torch.no_grad():
for paths in batched(tile_paths, 32):
tiles = [require_tile(Image.open(p)) for p in paths]
batch = torch.stack([image_processor(t) for t in tiles]).to(device)
chunks.append(model.predict_genes(batch, genes).cpu())
for paths in batched(tile_paths, 32):
tiles = [require_tile(Image.open(p)) for p in paths]
batch = torch.stack([image_processor(t) for t in tiles]).to(device)
chunks.append(model.predict_genes(batch, genes).cpu())
expression = torch.cat(chunks).numpy() # tiles by genes, log1p-CPM
```