primo-eval / quickstart.py
HungryTorch's picture
Point the Space at the PRIMOmics data repos
383d326 verified
Raw
History Blame Contribute Delete
5.31 kB
"""Produce a valid PRIMO submission in one command, then swap in your own model.
Downloads one modality's public datasets and writes the file the Submit tab
expects. The embedding here is deliberately dumb: bulk data uses
log2(CPM+1); single-cell data uses per-cell log2(CP10K+1), mean pooling by
collection sample, then PCA. Replace ``embed`` with your encoder and nothing else
changes.
pip install anndata scikit-learn pandas pyyaml huggingface_hub
python quickstart.py --modality bulk-rna --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 hf_hub_download, snapshot_download
from sklearn.decomposition import PCA
PUBLIC_REPO = "PRIMOmics/primo"
MANIFEST_FILENAME = "datasets.yaml"
MODALITIES = {"bulk-rna": "bulk RNA", "single-cell-rna": "Single Cell RNA"}
DATASET_ID = "dataset_id"
SAMPLE_ID = "sample_id"
TARGET_SUM = 1_000_000
SINGLE_CELL_TARGET_SUM = 10_000
N_COMPONENTS = 50
RANDOM_STATE = 0
def prediction_ids(adata: ad.AnnData) -> np.ndarray:
"""Opaque collection-sample ids required in the submission."""
if SAMPLE_ID in adata.obs:
return pd.unique(adata.obs[SAMPLE_ID].astype(str)).astype(str)
return adata.obs_names.astype(str).to_numpy()
def _log_normalize(x, target_sum: int) -> np.ndarray:
"""Return dense log2 counts-per-target expression."""
x = x.toarray() if hasattr(x, "toarray") else np.asarray(x)
totals = x.sum(axis=1, keepdims=True)
return np.log2(x / np.where(totals == 0, 1, totals) * target_sum + 1)
def _sample_expression(adata: ad.AnnData) -> tuple[np.ndarray, np.ndarray]:
"""Normalize expression and mean-pool cells into collection samples."""
ids = prediction_ids(adata)
if SAMPLE_ID not in adata.obs:
return ids, _log_normalize(adata.X, TARGET_SUM)
samples = adata.obs[SAMPLE_ID].astype(str).to_numpy()
pooled = [
_log_normalize(adata.X[samples == sample_id], SINGLE_CELL_TARGET_SUM).mean(0)
for sample_id in ids
]
return ids, np.asarray(pooled)
def embed(adata: ad.AnnData) -> np.ndarray:
"""One dataset's raw counts -> one vector per collection sample. Replace me.
Whatever you return, the contract is one finite row per id returned by
``prediction_ids``. The embedding width may differ between datasets.
"""
_, x = _sample_expression(adata)
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(modality: str, token: str | None) -> Path:
"""Download the manifest and expression files for one modality."""
manifest = Path(
hf_hub_download(
PUBLIC_REPO, MANIFEST_FILENAME, repo_type="dataset", token=token
)
)
paths = [
MANIFEST_FILENAME,
*[str(entry["path"]) for entry in datasets(manifest, modality)],
]
return Path(
snapshot_download(
PUBLIC_REPO, repo_type="dataset", token=token, allow_patterns=paths
)
)
def datasets(manifest_path: Path, modality: str) -> list[dict]:
"""Manifest entries for one modality."""
manifest = yaml.safe_load(manifest_path.read_text())
entries = manifest.get("datasets", []) if isinstance(manifest, dict) else manifest
return [entry for entry in entries if entry["modality"] == modality]
def build(root: Path, modality: str) -> pd.DataFrame:
"""Embed one modality into the 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 entry in datasets(root / MANIFEST_FILENAME, modality):
dataset_id = str(entry["id"])
adata = ad.read_h5ad(root / entry["path"])
sample_ids = prediction_ids(adata)
vectors = embed(adata)
print(
f"{dataset_id}: {adata.n_obs} observations, {len(sample_ids)} samples "
f"-> {vectors.shape[1]} dims"
)
block = pd.DataFrame(
vectors, columns=[f"e{i}" for i in range(vectors.shape[1])]
)
block.insert(0, SAMPLE_ID, sample_ids)
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("--modality", required=True, choices=MODALITIES)
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()
modality = MODALITIES[args.modality]
submission = build(download(modality, args.token), modality)
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()