primo-eval / quickstart.py
HungryTorch's picture
Ship redesigned PRIMO Space (rail nav, primo.css) reading PRIMOmics data
c9d0ebc verified
Raw
History Blame Contribute Delete
3.67 kB
"""Produce a valid PRIMO submission in one command, then swap in your own model.
Downloads every public dataset, embeds each one, and writes the single file the
Submit tab expects. The embedding here is deliberately dumb -- log2(CPM+1) then
PCA -- because the point is the plumbing, not the score: replace ``embed`` with
your encoder and nothing else changes.
pip install anndata scikit-learn pandas pyyaml huggingface_hub
python quickstart.py --out submission.parquet
Standalone on purpose: no import from this Space and none from our monorepo, so
it keeps working if you copy the file into your own project.
"""
import argparse
from pathlib import Path
import anndata as ad
import numpy as np
import pandas as pd
import yaml
from huggingface_hub import snapshot_download
from sklearn.decomposition import PCA
PUBLIC_REPO = "PRIMOmics/primo"
MANIFEST_FILENAME = "datasets.yaml"
DATASET_ID = "dataset_id"
SAMPLE_ID = "sample_id"
TARGET_SUM = 1_000_000
N_COMPONENTS = 50
RANDOM_STATE = 0
def embed(adata: ad.AnnData) -> np.ndarray:
"""One dataset's raw counts -> one vector per patient. Replace me.
Whatever you return, the contract is the same: one row per sample, in
``adata.obs_names`` order, all finite. The embedding width is yours to pick
and may differ from one dataset to the next.
"""
x = adata.X
x = x.toarray() if hasattr(x, "toarray") else np.asarray(x)
x = x.astype(float)
counts = x.sum(axis=1, keepdims=True)
x = np.log2(x / np.where(counts == 0, 1, counts) * TARGET_SUM + 1)
k = min(N_COMPONENTS, x.shape[0] - 1, x.shape[1])
return PCA(n_components=k, random_state=RANDOM_STATE).fit_transform(x)
def download(token: str | None) -> Path:
"""Pull the public benchmark (manifest + every ``expression.h5ad``)."""
return Path(snapshot_download(PUBLIC_REPO, repo_type="dataset", token=token))
def dataset_ids(root: Path) -> list[str]:
"""The opaque ids to embed, read off the public manifest."""
manifest = yaml.safe_load((root / MANIFEST_FILENAME).read_text())
entries = manifest.get("datasets", []) if isinstance(manifest, dict) else manifest
return [str(entry["id"]) for entry in entries]
def build(root: Path) -> pd.DataFrame:
"""Embed every dataset into the one frame the Submit tab expects.
Datasets of different widths stack into one table; the extra columns of a
narrower dataset stay empty and the evaluator drops them per dataset, so each
dataset keeps its own embedding size.
"""
blocks = []
for dataset_id in dataset_ids(root):
adata = ad.read_h5ad(root / dataset_id / "expression.h5ad")
vectors = embed(adata)
print(f"{dataset_id}: {adata.n_obs} samples -> {vectors.shape[1]} dims")
block = pd.DataFrame(
vectors, columns=[f"e{i}" for i in range(vectors.shape[1])]
)
block.insert(0, SAMPLE_ID, adata.obs_names.to_numpy())
block.insert(0, DATASET_ID, dataset_id)
blocks.append(block)
return pd.concat(blocks, ignore_index=True)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--out", type=Path, default=Path("submission.parquet"))
parser.add_argument("--token", default=None, help="HF token, if you need one.")
args = parser.parse_args()
submission = build(download(args.token))
if args.out.suffix == ".csv":
submission.to_csv(args.out, index=False)
else:
submission.to_parquet(args.out, index=False)
print(f"\nWrote {args.out}: {len(submission)} rows. Upload it on the Submit tab.")
if __name__ == "__main__":
main()