references/api.md
# DeepSpot-M API reference
Reviewed against the released `deepspotm==1.0.0` wheel and matching current
upstream `model.py` on 2026-09-30. Weight-dependent examples are illustrative;
see the verification scope in `SKILL.md`.
## Loading and reproducibility
```python
from deepspotm import DeepSpotM
model, image_processor = DeepSpotM.from_pretrained(
"ratschlab/DeepSpotM",
source="scgpt",
device="cpu",
revision="48be27af436a50e5c74175680ac2b7b2596a506b",
)
```
Exact signature: `from_pretrained(repo_id_or_path, *, source=None, device=None,
revision=None)`. It downloads `config.json`, `model.safetensors` and `tokens.csv`
from the specified Hub revision, returns `(model, image_processor)`, puts the
model in evaluation mode and uses center-crop evaluation transforms. The Midnight
backbone is built from a bundled configuration; it does not separately download
`kaiko-ai/midnight` weights for this route.
- `device=None` chooses CUDA when available, otherwise CPU. The processor returns a
CPU tensor; move the batch to the same device. `predict_genes` does not do this.
- `source` selects a loaded embedding pathway. Supply one of `evo2`, `orthrus`,
`prott5`, `scgpt`, `apertus` for the published multi-source model. Omitting it
with several pathways raises `ValueError`.
- `revision` is a Hub branch/tag/commit; record an immutable commit for reproducibility.
It has no effect when loading a local directory.
- A local directory must contain the three compatible files above. There are no
`token`, `cache_dir`, `local_files_only`, or arbitrary Hub keyword arguments on
this method. Use Hub authentication/environment settings, or download an approved
snapshot separately and load its directory.
Authenticate with `hf auth login` only after the manual access request is approved.
The current gate also requires eligibility declarations; consult the live model page.
Cached credentials or `HF_TOKEN` are read by `hf_hub_download`. Configure `HF_HOME`
before imports, and keep access-limited caches restricted to approved users. Setting
`HF_HOME` alone does not guarantee offline operation: use a complete local directory
on a node without network access. Never mirror weights to bypass the gate.
Do not translate every exception into an authentication error. Distinguish an absent
package, a denied Hub request, missing local files, invalid source, incompatible
checkpoint and device/memory errors; retain the original exception as the cause.
An import error can arise from a broken transitive dependency even when DeepSpot-M
itself is installed.
## Gene queries and device placement
```python
import torch
# pil_tiles have already passed size, RGB and physical-resolution validation.
genes = ["EPCAM", "CD3D", "PTPRC"]
if not genes or len(genes) != len(set(genes)):
raise ValueError("Use a nonempty list of unique gene symbols")
missing = [g for g in genes if g not in set(model.gene_names)]
if missing:
raise ValueError(f"Not in the DeepSpot-M panel: {missing}")
batch = torch.stack([image_processor(t) for t in pil_tiles]).to(model.device)
vals = model.predict_genes(batch, genes).cpu()
if vals.shape != (len(pil_tiles), len(genes)) or not torch.isfinite(vals).all():
raise ValueError("Unexpected prediction shape or nonfinite values")
```
`predict_genes(pixel_values, genes)` accepts processed `(B, 3, 224, 224)` tensors
and either one gene string or an ordered sequence. It returns `(B, G)` on the input/model
device, preserving requested order; a single string yields `(B, 1)`. The method uses
`torch.no_grad()` internally. It does not change the model's training/evaluation mode.
`from_pretrained` already calls `.eval()`; retain that setting for inference.
The checkpoint's `model.gene_names` is authoritative for membership and spelling.
Resolve HGNC aliases explicitly, preserve the alias-to-panel mapping, and do not
assume every modern symbol is present. Unknown symbols raise `KeyError`. The
released panel covers approximately 19,000 protein-coding genes; zero-shot prediction
refers to panel genes unseen during training, not arbitrary novel query symbols.
Duplicate queries repeat output columns; reject them when assembling unique variables.
`model(batch)` returns three values `(expression, pooled, attention_weights)`;
full-panel expression column `i` maps to `model.gene_names[i]`. The skill uses
`predict_genes` to avoid computing unneeded queries. A single query call reuses one
backbone pass across the requested genes. If memory requires gene chunking, repeated
`predict_genes` calls also repeat that backbone pass; do not assume a public token cache.
## Source comparisons
| `source` | Embedding origin |
| --- | --- |
| `evo2` | Genomic sequence |
| `orthrus` | RNA |
| `prott5` | Protein sequence |
| `scgpt` | Single-cell expression |
| `apertus` | Language model |
The published multi-source model exposes `model.sources`, `model.current_source`
and `model.set_source(name)`. Changing the source does not require reloading the
processor or weights. Run sequentially; this mutates the shared model and is not
safe to switch concurrently between requests.
```python
per_source = {}
for source in ("scgpt", "prott5", "evo2"):
model.set_source(source)
per_source[source] = model.predict_genes(batch, genes).cpu()
model.set_source("scgpt")
```
Keep one source fixed for a primary cohort analysis; treat alternative sources as
sensitivity analyses, not automatically exchangeable measurements or uncertainty estimates.
## Memory and output scale
Start with a few tiles and a short gene list, then measure peak memory before increasing
batch size. A batch of 32 tiles and all ~19k queries can be much larger than a marker
panel; no universally safe batch size is established here. Persist slide outputs as
you go. Dense float32 expression alone costs `4 * n_tiles * n_genes` bytes, before
intermediate tensors or duplicate arrays during concatenation.
Values are predicted **log1p-CPM**, not observed counts. Preserve this in metadata;
do not normalize/log them again or pass them to count likelihoods. Inverse-transform
only when needed for interpretation:
```python
import numpy as np
cpm_scale = np.expm1(vals.numpy()) # vals was moved to CPU above
```
The decoder has an unconstrained regression output; inspect negative predictions,
nonfinite values and overflow rather than silently clipping them. Inverted values
are not guaranteed nonnegative or to sum to one million, especially for a subset
of genes. Report any postprocessing separately. Validate slide/patient-held-out
performance for the tissue, stain and processing conditions of interest.
## Primary sources
- [Released package metadata](https://pypi.org/pypi/deepspotm/1.0.0/json)
- [Reviewed upstream model source](https://github.com/ratschlab/DeepSpotM/blob/4d7793c890c500f51033444cc755f02b9d523628/src/deepspotm/model.py)
- [Model card and access declarations](https://huggingface.co/ratschlab/DeepSpotM)
- [Hub authentication CLI](https://huggingface.co/docs/huggingface_hub/en/guides/cli)
- [Hub environment variables](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables)
- [Study preprint](https://doi.org/10.64898/2026.06.19.26356060)
references/whole_slide.md
# Whole-slide and cohort runs
This recipe separates optional **histolab 0.7.0 tiling** (Python 3.10/3.11 and
OpenSlide) from **DeepSpot-M inference** (tested API stack on Python 3.12).
Exchange tiles plus manifests between environments. Install AnnData, NumPy, pandas
and Pillow in the inference environment for export; matplotlib is only needed to plot.
Full WSI extraction and gated inference are illustrative, not an executed clinical
or biological validation. Local synthetic checks cover the boundary logic and export.
## 1. Verify physical resolution
A 224x224 tile at 0.5 microns/pixel spans about 112x112 microns. Do not assume
level 0 is 20x or level 1 is half its resolution: pyramid factors vary by scanner.
Check both axes and reject missing/invalid metadata. This native-level example uses
an explicit 10% engineering tolerance; choose and validate that tolerance for the
study, rather than interpreting it as an established model accuracy guarantee.
```python
import math
import openslide
def select_native_level(properties, downsamples, target=0.5, tolerance=0.10):
mpp = [float(properties.get(f"openslide.mpp-{axis}", "nan")) for axis in ("x", "y")]
if not all(math.isfinite(v) and v > 0 for v in mpp):
raise ValueError("Need verified positive MPP for both slide axes")
factors = [float(v) for v in downsamples]
if not factors or not all(math.isfinite(v) and v > 0 for v in factors):
raise ValueError("Invalid slide pyramid downsample factors")
level = min(range(len(factors)), key=lambda i: max(
abs(v * factors[i] / target - 1) for v in mpp
))
actual = [v * factors[level] for v in mpp]
if any(abs(v / target - 1) > tolerance for v in actual):
raise ValueError("No matching native level; explicitly resample from finer pixels")
return level, mpp, actual
with openslide.open_slide("slide.svs") as raw_slide:
level, level0_mpp, actual_mpp = select_native_level(
raw_slide.properties, raw_slide.level_downsamples
)
level0_dimensions = raw_slide.dimensions
```
If no native level qualifies, stop this recipe and use a separately verified resampling
workflow. Histolab exposes `mpp=...` but that path has additional backend requirements;
it is not exercised here. Record the original metadata, source level, resampling
method and effective MPP. Upsampling coarse pixels cannot recover missing detail.
## 2. Tile and create a coordinate manifest
Use a **new, empty directory per slide/run**; stale PNGs would corrupt assembly.
The tissue mask and 80% tissue threshold are analysis choices: inspect a thumbnail
with tile outlines, especially for small fragments, sparse tissues and artifacts.
The default `BiggestTissueBoxMask` can exclude separate tissue fragments; this
example explicitly requests `TissueMask`.
```python
from pathlib import Path
from histolab.slide import Slide
from histolab.tiler import GridTiler
from histolab.masks import TissueMask
tile_dir = Path("tiles")
tile_dir.mkdir(exist_ok=False)
slide = Slide("slide.svs", processed_path=str(tile_dir))
tiler = GridTiler(
tile_size=(224, 224), level=level, check_tissue=True,
tissue_percent=80.0, pixel_overlap=0,
)
tiler.extract(slide, extraction_mask=TissueMask())
```
Released GridTiler 0.7.0 returns `None` and writes coordinate-bearing filenames;
it does not provide a `report_path` argument. Build this explicit manifest from
that documented naming contract. The bounds are in **level-0 pixels**.
```python
import csv
import json
import re
pattern = re.compile(r"tile_\d+_level(\d+)_(\d+)-(\d+)-(\d+)-(\d+)\.png")
rows = []
for path in sorted(tile_dir.glob("*.png")):
match = pattern.fullmatch(path.name)
if not match:
raise ValueError(f"Unexpected GridTiler filename: {path.name}")
tile_level, x0, y0, x1, y1 = map(int, match.groups())
if tile_level != level or not (0 <= x0 < x1 <= level0_dimensions[0]
and 0 <= y0 < y1 <= level0_dimensions[1]):
raise ValueError(f"Invalid level or bounds: {path.name}")
rows.append([path.name, x0, y0, x1, y1, tile_level])
if not rows:
raise ValueError("No tissue tiles survived extraction")
with open("tiles_report.csv", "w", newline="", encoding="utf-8") as handle:
writer = csv.writer(handle)
writer.writerow(["tile_name", "x0", "y0", "x1", "y1", "level"])
writer.writerows(rows)
Path("tiling.json").write_text(json.dumps({
"slide_id": "slide", "level": level, "level0_mpp": level0_mpp,
"actual_mpp": actual_mpp, "coordinate_units": "level0_pixels",
"bounds_origin": "upper_left", "tile_px": 224,
"tissue_mask": "TissueMask", "tissue_percent": 80.0,
}), encoding="utf-8")
```
## 3. Predict in the inference environment
Use the same gene order throughout. Start with a small batch; scale only after
measuring memory for the actual gene count. This in-memory example suits marker
panels/modest slides. A full-transcriptome WSI may require chunked persistence;
`torch.cat` temporarily duplicates stored expression chunks.
```python
from pathlib import Path
import json
import torch
from PIL import Image
from deepspotm import DeepSpotM
metadata = json.loads(Path("tiling.json").read_text(encoding="utf-8"))
tile_paths = sorted(Path("tiles").glob("*.png"))
if not tile_paths:
raise ValueError("No input tiles")
genes = ["EPCAM", "CD3D", "PTPRC", "MKI67"]
source = "scgpt"
revision = "48be27af436a50e5c74175680ac2b7b2596a506b"
device = "cuda" if torch.cuda.is_available() else "cpu"
model, image_processor = DeepSpotM.from_pretrained(
"ratschlab/DeepSpotM", source=source, device=device, revision=revision
)
if not genes or len(genes) != len(set(genes)) or not set(genes) <= set(model.gene_names):
raise ValueError("Use unique genes from the loaded panel")
chunks = []
batch_size = 4
for start in range(0, len(tile_paths), batch_size):
paths = tile_paths[start:start + batch_size]
tensors = []
for path in paths:
with Image.open(path) as tile:
if tile.size != (224, 224):
raise ValueError(f"Wrong tile dimensions: {path.name}")
tensors.append(image_processor(tile.convert("RGB")))
batch = torch.stack(tensors).to(device)
values = model.predict_genes(batch, genes).cpu()
if values.shape != (len(paths), len(genes)) or not torch.isfinite(values).all():
raise ValueError("Unexpected prediction shape or nonfinite values")
chunks.append(values)
expression = torch.cat(chunks).numpy() # tiles x genes, predicted log1p-CPM
```
## 4. Assemble and write the slide map
Join by tile ID, validate exact coverage, and store **tile centers** rather than
silently mixing origin/center coordinates. Inspect the raw CSV header before pandas
can rename duplicates. This block continues from step 3.
```python
import csv
import anndata as ad
import numpy as np
import pandas as pd
with open("tiles_report.csv", newline="", encoding="utf-8") as handle:
header = next(csv.reader(handle))
if header != ["tile_name", "x0", "y0", "x1", "y1", "level"]:
raise ValueError("Unexpected or duplicated manifest columns")
report = pd.read_csv("tiles_report.csv", dtype={"tile_name": str}).set_index(
"tile_name", verify_integrity=True
)
tile_names = [path.name for path in tile_paths]
if len(tile_names) != len(set(tile_names)) or set(report.index) != set(tile_names):
raise ValueError("Manifest must match unique predicted tile IDs exactly")
if expression.shape != (len(tile_names), len(genes)) or not np.isfinite(expression).all():
raise ValueError("Invalid prediction matrix")
report = report.loc[tile_names]
boxes = report[["x0", "y0", "x1", "y1"]].to_numpy(dtype=float)
if not np.isfinite(boxes).all() or (boxes < 0).any() or (boxes[:, 2:] <= boxes[:, :2]).any():
raise ValueError("Invalid coordinate bounds")
if not (report["level"] == metadata["level"]).all():
raise ValueError("Manifest and tiling metadata levels disagree")
coords = (boxes[:, :2] + boxes[:, 2:]) / 2
adata = ad.AnnData(
X=expression, obs=report.copy(),
var=pd.DataFrame(index=pd.Index(genes, name="gene")),
)
adata.obsm["spatial"] = coords
adata.uns["deepspotm"] = {
**metadata, "source": source, "model_repo": "ratschlab/DeepSpotM",
"revision": revision, "package_version": "1.0.0", "units": "log1p-CPM",
"measurement_type": "model_prediction", "spatial_origin": "tile_center",
}
output = Path("slide.h5ad")
temporary = output.with_name(output.stem + ".partial.h5ad")
adata.write_h5ad(temporary)
check = ad.read_h5ad(temporary)
if check.shape != adata.shape or list(check.obs_names) != tile_names:
raise ValueError("H5AD verification failed")
temporary.replace(output)
```
A successful write is structural evidence only. Inspect image/map alignment and
validate prediction performance against held-out paired assays. Do not treat these
values as observed counts, and do not log-normalize them again. Negative predictions
are possible; report any transformations rather than silently clipping them.
## 5. Plot and scale to cohorts
```python
import matplotlib.pyplot as plt
values = np.asarray(adata[:, "EPCAM"].X).ravel()
plt.scatter(coords[:, 0], coords[:, 1], c=values, s=6, cmap="viridis")
plt.gca().invert_yaxis()
plt.gca().set_aspect("equal")
plt.colorbar(label="Predicted EPCAM (log1p-CPM)")
```
For cohorts, reuse one loaded model per inference worker and write one verified
H5AD per slide. Resume only after checking the saved shape, IDs and run provenance
(model revision, source, genes, tile/slide identity, MPP and preprocessing), not
merely the existence of a file. Avoid one process per slide sharing a single GPU
without an explicit memory/concurrency plan.
If combining slides, `ad.concat({slide_id: data, ...}, label="slide_id",
index_unique="::", join="inner")` supplies a slide label and unique observation IDs.
Check that all input gene lists match first; preserve per-slide provenance separately
because default `uns` merging does not retain it. Spatial coordinates remain local to
each slide: build spatial neighbor graphs separately rather than placing different
slides into one coordinate system. Split validation by patient/slide, not by tile.
The released base model differs from the cancer-specific fine-tuned models used for
the reported TCGA atlas; this recipe does not reproduce that atlas by itself.
## Primary sources
- [DeepSpot-M source and release context](https://github.com/ratschlab/DeepSpotM)
- [Upstream WSI example](https://github.com/ratschlab/DeepSpotM/blob/4d7793c890c500f51033444cc755f02b9d523628/examples/predict_wsi.py)
- [Histolab released metadata](https://pypi.org/pypi/histolab/0.7.0/json)
- [Histolab GridTiler reference](https://histolab.readthedocs.io/en/latest/api/tiler.html)
- [OpenSlide Python](https://openslide.org/api/python/)
- [AnnData](https://anndata.readthedocs.io/en/stable/generated/anndata.AnnData.html)
- [AnnData concatenation](https://anndata.readthedocs.io/en/stable/generated/anndata.concat.html)
SKILL.md
---
name: deepspot-m
description: Generates transcriptome-wide virtual spatial transcriptomics from H&E histology with DeepSpot-M. Used for predicted log1p-CPM expression from 224x224 tiles at about 20x, querying the released protein-coding gene panel by symbol, and whole-slide prediction after resolution-aware tiling with histolab.
license: PolyForm-Noncommercial-1.0.0
compatibility: Requires deepspotm 1.0.0, Python >=3.10 and PyTorch; network and approved Hugging Face access for initial gated weight download. CPU supported; CUDA optional. Optional histolab 0.7.0 tiling requires a separate Python 3.10/3.11 environment and OpenSlide. AnnData is needed for H5AD export.
allowed-tools: Read Write Edit Bash
metadata:
version: "1.2"
skill-author: Ratschlab, ETH Zurich
last-reviewed: "2026-09-30"
---
# DeepSpot-M
## Overview
DeepSpot-M is a multimodal foundation model that maps a 224x224 H&E histology tile to
spatial gene expression in log1p-CPM. The output is virtual spatial transcriptomics: one
value per queried gene per tile, laid out on the grid the tiles came from.
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 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.
## Licensing
The code is PolyForm Noncommercial 1.0.0 and the weights are CC-BY-NC-SA-4.0.
The current model access form additionally restricts eligibility to academic/public
nonprofit researchers without concurrent commercial affiliations and excludes
commercially funded/collaborative use. Read the live declarations before requesting
access; a noncommercial intention alone does not establish eligibility. Research use
only, not clinical or diagnostic use. Preserve the applicable attribution and licenses.
## Installation
```bash
uv pip install deepspotm==1.0.0
```
PyPI 1.0.0 declares Python >=3.10 without an upper bound; that is not proof of
compatibility with every future dependency release. Use an isolated environment.
Install the PyTorch build that matches your CUDA runtime if using a GPU.
The optional histolab 0.7.0 tiler requires Python <3.12 and an older scientific stack:
run tiling separately and exchange PNG tiles plus a coordinate manifest.
## Model access
The weights are gated:
1. Open <https://huggingface.co/ratschlab/DeepSpotM> and request access.
2. Once access is granted, authenticate the machine that will download them:
```bash
hf auth login
```
The Hugging Face downloader uses cached credentials or `HF_TOKEN`; approval and
authentication are separate. Set `HF_HOME` before importing Hub libraries if choosing
a cache location. Do not put tokens in code. An approved local model directory can
be loaded offline.
## Quick start
Illustrative gated inference; the loading/query API and local preprocessing were
checked without downloading the weights:
```python
import torch
from PIL import Image
from deepspotm import DeepSpotM
device = "cuda" if torch.cuda.is_available() else "cpu"
revision = "48be27af436a50e5c74175680ac2b7b2596a506b"
model, image_processor = DeepSpotM.from_pretrained(
"ratschlab/DeepSpotM", source="scgpt", device=device, revision=revision
)
with Image.open("tile.png") as image:
if image.size != (224, 224):
raise ValueError("Expected a pre-tiled 224x224 image at verified ~0.5 um/px")
batch = image_processor(image.convert("RGB")).unsqueeze(0).to(device)
genes = ["EPCAM", "CD3D"]
vals = model.predict_genes(batch, genes).cpu() # shape (1, 2)
```
`image_processor` turns a PIL image 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. Symbols must be
in the released ~19k-gene panel (`model.gene_names`); an unknown symbol raises `KeyError`
naming the offending genes.
## Tile requirements
Tiles must be 224x224 RGB at roughly 20x magnification (about 0.5 microns per pixel). Check
the size at the boundary of your pipeline rather than passing an unchecked crop through:
```python
TILE_PX = 224
def require_tile(tile):
"""Return an RGB 224x224 tile, or raise if the crop is the wrong size."""
if tile.size != (TILE_PX, TILE_PX):
raise ValueError(
f"DeepSpot-M expects a {TILE_PX}x{TILE_PX} tile at about 20x "
f"(~0.5 microns per pixel); got {tile.size[0]}x{tile.size[1]}. "
"Re-tile at the matching level or resample the crop."
)
return tile.convert("RGB")
```
Pixel size alone does not verify magnification. Read both MPP axes and the actual
level downsample factors; use a native level only if its physical resolution is
close enough to the chosen target. Otherwise explicitly resample from sufficiently
fine source pixels and record the transform. Never infer 20x from a level number.
The returned processor resizes/crops images but cannot establish their physical MPP.
## Keep the dependency optional
`deepspotm` and its weights are a heavy, gated dependency. Import it inside the function
that needs it so the surrounding project installs, imports and tests without it, and turn
an `ImportError` into a message that names every step:
```python
DEEPSPOTM_HELP = (
"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 `hf auth login`."
)
def load_deepspotm(source="scgpt"):
try:
from deepspotm import DeepSpotM
except ImportError as exc:
raise RuntimeError(DEEPSPOTM_HELP) from exc
return DeepSpotM.from_pretrained("ratschlab/DeepSpotM", source=source)
```
## Embedding sources
`source` selects which frozen gene embedding the router builds projections from. It is one
of five values:
| `source` | Gene embedding |
| --------- | --------------------------------- |
| `evo2` | genomic sequence |
| `orthrus` | RNA |
| `prott5` | protein sequence |
| `scgpt` | single-cell expression |
| `apertus` | language model |
Each gives a different view of gene identity. Pick one per run, and run the same tiles
through more than one source when the choice matters to your analysis. The released
multi-source model can switch using `model.set_source(name)`; no reload is required.
See
`references/api.md` for the inference call surface, batching and device placement, gene symbol
handling and output units.
## Whole slide workflow
Prediction is per tile, so a slide-scale run is a tiling step followed by batched
inference:
1. Verify slide MPP, extract 224x224 tiles on a tissue grid with the `histolab` skill,
and keep each tile's level-0 bounding box and measured output MPP.
2. Process and stack tiles into batches with `torch.stack`.
3. Call `predict_genes` once per batch with the same gene list.
4. Concatenate batches in a recorded tile-ID order; join coordinates by those IDs,
checking uniqueness and missing tiles rather than assuming file/report row order.
Keep outputs labeled as model predictions, not measured transcript counts. Validate
on held-out slides/patients with paired assays for the intended tissue and processing
conditions; tiles from one slide are not independent biological replicates. The
released base model also differs from cancer-specific fine-tuned TCGA atlas models.
That matrix is the virtual spatial transcriptomics map for the slide, and it drops
into `AnnData` for downstream analysis. [Whole-slide workflow](references/whole_slide.md)
has a worked loop, batch sizing and an `AnnData` assembly step.
## Verification scope
Reviewed the current PyPI 1.0.0 wheel and matching upstream model source on
2026-09-30. CPU checks used Python 3.12.10, Torch 2.7.1, Torchvision 0.22.1,
Transformers 5.18.0, PEFT 0.21.1, Lightning 2.6.6 and Hugging Face Hub 1.33.0.
Synthetic checks cover loading/query contracts, normalization, resolution rejection,
coordinate joins and H5AD round-trip. Gated weights, CUDA, real-slide tiling and
biological prediction accuracy were not tested; full inference examples are illustrative.
## Common use cases
- Spatial expression maps for marker genes across a tumour section.
- Transcriptome-wide prediction over a slide cohort with no matching assay run.
- 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.
## Detailed references
- `references/api.md`: `from_pretrained` and `predict_genes` in full, the five embedding
sources and how to choose, batching, device placement, gene symbol handling, and
converting log1p-CPM output.
- `references/whole_slide.md`: tiling with histolab, a slide-scale prediction loop,
assembling and storing a tiles-by-genes matrix, and cohort-scale runs.
## Primary sources
- Paper: <https://doi.org/10.64898/2026.06.19.26356060> (medRxiv, 2026)
- Code: <https://github.com/ratschlab/DeepSpotM>
- Weights: <https://huggingface.co/ratschlab/DeepSpotM>
- PyPI: <https://pypi.org/project/deepspotm/>