Adding a method¶
How to wrap a model so that scfoundry --method <id> runs it. Expect a day for a model
with a clean Python API, and longer for one that ships as research scripts.
Before you start¶
Work in a development checkout:
git clone,pip install -e .,scfoundry init .— see Developer install. Thescfoundrycommand then runs the pipeline from your checkout, anddata/demo/is in place.Read the model’s own documentation for zero-shot embedding and note the exact preprocessing, tokenisation, pooling and checkpoint it uses. That recipe is what you implement — not what you think works better.
Check the licence of the code and of the weights. Some checkpoints are restricted to non-commercial use; the method’s registry entry should say so.
Pick a short lowercase id (
newmodel). It becomes the--methodvalue, the module name, the output directory and the registry key.
1. The container¶
Every method runs in its own image, so the model’s dependency pins never meet anyone
else’s. Write containers/newmodel/Dockerfile:
FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-pip git \
&& rm -rf /var/lib/apt/lists/*
# The model's own code at a pinned revision, plus what the module needs to read and
# write AnnData. Pin everything the model pins.
RUN pip3 install --no-cache-dir \
torch==2.3.1 --index-url https://download.pytorch.org/whl/cu121 \
&& pip3 install --no-cache-dir \
"newmodel @ git+https://github.com/lab/newmodel.git@a1b2c3d" \
anndata==0.10.8 scanpy==1.10.2
docker build -t yourname/newmodel:0.1.0 containers/newmodel
docker push yourname/newmodel:0.1.0
Requirements the pipeline relies on:
python3(orpython) onPATH, withanndataandscanpyimportable, because the module’s scripts are Python;a CUDA runtime matching the driver floor (525) for GPU methods;
no assumption about
$HOME: containers run with--no-home --cleanenv, and a stand-in home is mounted. Code that writes caches to~/.cacheworks; code that reads your dotfiles does not.
Test it before writing any Nextflow: apptainer exec --nv docker://yourname/newmodel:0.1.0 python3 -c "import newmodel, scanpy".
2. The weights¶
Add a process to workflows/utils/download.nf that fetches the official checkpoint into
/data/model_weights/<Name>/, which is the workspace’s data/model_weights/ bind-mounted:
process download_newmodel_checkpoints {
container "housy17/scfm_download:latest"
output:
stdout
script:
"""
cd /data/model_weights
mkdir -p NewModel && cd NewModel
hf download lab/newmodel model.ckpt --revision a1b2c3d4e5f6 --local-dir ./
echo "NewModel checkpoints downloaded!"
"""
}
The download image has hf (Hugging Face), gdown, curl and tar. Pin the revision,
and verify a checksum when the source cannot pin one for you — the CellPLM and scPRINT
processes in the same file show both patterns. Then add the process to the runners map
of workflows/tasks/download.nf.
The path under data/model_weights/ becomes the module’s params.model default.
3. The module¶
workflows/methods/newmodel.nf declares the method’s parameters and its processes. The
minimal module is one process; pca.nf (55 lines) is the simplest real one, and
scimilarity.nf shows the common two-step shape — a preprocessing process and an
embedding process, wrapped in a named workflow.
params.model = "NewModel/model.ckpt" // under /data/model_weights
params.batch_size = 64
params.emb_results_dir = "results"
process embed_by_newmodel {
tag "${id}"
label 'gpu_task' // or 'cpu_task'
container "yourname/newmodel:0.1.0"
publishDir "${params.emb_results_dir}/embeddings/newmodel", mode: 'copy',
saveAs: { filename -> "${id}.h5ad" }, enabled: params.emb_results_dir as boolean
input:
tuple val(id), path(raw_h5ad)
output:
tuple val(id), val("NewModel"), path("*_embeddings.h5ad")
script:
"""
#!/usr/bin/env python3
import numpy as np
import pandas as pd
import scanpy as sc
import newmodel
adata = sc.read_h5ad("${raw_h5ad}")
obs, spatial = adata.obs.copy(), adata.obsm.get("spatial")
# --- the authors' recipe, exactly ---------------------------------------
model = newmodel.load("/data/model_weights/${params.model}")
embedding = model.embed(adata, batch_size=${params.batch_size}) # (n_cells, n_dims)
# -------------------------------------------------------------------------
out = sc.AnnData(
X=np.asarray(embedding, dtype=np.float32),
obs=obs,
var=pd.DataFrame(index=[f"V{i + 1}" for i in range(embedding.shape[1])]),
)
if spatial is not None:
out.obsm["spatial"] = spatial
out.write_h5ad("newmodel_embeddings.h5ad")
"""
}
The parts that are not negotiable:
Element |
Contract |
|---|---|
|
|
|
|
The embedding file |
|
|
|
|
|
|
Every knob a user may want, with the authors’ default. Method-specific names get the
method as prefix ( |
Weights and code paths |
Weights under |
If the model needs a separate preprocessing step (tokenisation, gene alignment), make it
its own process — it can then run on the CPU and be retried independently — and expose a
workflow embed_by_newmodel that chains the two, as scimilarity.nf does. The task maps
call embed_by_newmodel(ch) either way.
Fine-tuning¶
Only add fine-tuning if the authors publish a recipe that updates the model’s parameters. It is two more processes:
finetune_by_newmodel—input: tuple val(id), path(raw_h5ad)with labels inobs[params.finetune_label_key];output: tuple val(id), path("<model dir>"), published by copy to<finetune_results_dir>/finetune/finetuned_models/newmodel/<id>/. Declareparams.finetune_epoch,params.finetune_batch_size,params.finetune_eval_size = 0.2andparams.predict_batch_sizewith the authors’ values.predict_by_newmodel—input: tuple val(id), path(query_h5ad), path(model_dir); writes<id>_predicted_labels.tsv(columns:barcodeindex,predicted_label) and<id>_predicted_probs.tsv(one column per class), published to<finetune_results_dir>/finetune/prediction/newmodel/.
Look at scgpt.nf for a complete native example.
4. The registry¶
One line in conf/methods.json:
"newmodel": {"name": "NewModel", "category": "zero-shot", "container": "yourname/newmodel:0.1.0",
"gpu": true, "tasks": ["download", "embed", "transfer"], "notes": ""}
category is zero-shot, reference or integration; tasks lists what the module
implements. scfoundry list methods and the launcher’s argument checks read this file, so
a method that is not registered does not exist as far as the command is concerned.
5. The task maps¶
Nextflow includes are static, so each task workflow names the modules it can call. Add an
include line and a map entry to every task in the registry entry’s tasks:
File |
What to add |
|---|---|
|
|
|
|
|
The same include, and the same entry in |
|
Include |
The registry test enforces that every map agrees with conf/methods.json, so a forgotten
entry fails immediately rather than at runtime:
python -m unittest tests/test_registry.py tests/test_launcher.py
6. Verify on the demo data¶
scfoundry list methods --task embed # newmodel is listed
scfoundry download --method newmodel
scfoundry embed --method newmodel --data data/demo/colon_1000.h5ad
Check the output against the contract:
import anndata as ad, numpy as np
a = ad.read_h5ad("results/embeddings/newmodel/colon_1000.h5ad")
raw = ad.read_h5ad("data/demo/colon_1000.h5ad")
assert a.shape[0] == raw.n_obs and (a.obs_names == raw.obs_names).all()
assert a.X.dtype == np.float32 and not np.isnan(a.X).any()
assert list(a.var_names[:2]) == ["V1", "V2"]
print(a.shape)
Then score it, and read the numbers next to PCA’s — an embedding that scores far below PCA on the demo is more often a preprocessing mistake than a bad model:
scfoundry benchmark --embedding results/embeddings/newmodel --batch-key batch_id
scfoundry benchmark --embedding results/embeddings/pca --batch-key batch_id
If transfer is declared, run the one-shot liver pair; if finetune is, the colon pair.
Finally, confirm you reproduce something the authors published — the embedding of their
tutorial dataset, a reported accuracy — so that “the authors’ recipe” is a checked claim.
7. Document it¶
In the documentation repository: a row in docs/_data/methods.csv and
docs/_data/support_matrix.csv, a row on the citation page, the
weight size and source on Downloading model weights,
and the method-specific parameters on the method reference.
See also
Contributing a method — getting it into the main repository.
Output reference — the file formats in full.