Spaces:
Running
Running
| """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() | |