Download scripts/fetch_sciplex_subset.py from ChatterjeeLab/ReMEDi: direct link, hf CLI and curl.
- Browser
- Download file 6.38 kB
-
https://huggingface.co/ChatterjeeLab/ReMEDi/resolve/main/scripts/fetch_sciplex_subset.py
- Command line
-
hf download hf://ChatterjeeLab/ReMEDi/scripts/fetch_sciplex_subset.py
-
curl -L -o fetch_sciplex_subset.py https://huggingface.co/ChatterjeeLab/ReMEDi/resolve/main/scripts/fetch_sciplex_subset.py
6.38 kB
| """Fetch a bounded sci-Plex3 prototype by HTTP ranges, with exact-name Tahoe SMILES. | |
| The source is the corrected scPerturb object. The selected cohort is intentionally | |
| small and is an engineering prototype, not the external ICML benchmark. | |
| """ | |
| import argparse | |
| from remedi.download import RangeReader | |
| from pathlib import Path | |
| import json | |
| import hashlib | |
| import time | |
| import numpy as np | |
| import pandas as pd | |
| import requests | |
| import fsspec | |
| import h5py | |
| import anndata as ad | |
| import pyarrow.parquet as pq | |
| from scipy import sparse | |
| URL = "https://zenodo.org/records/13350497/files/SrivatsanTrapnell2020_sciplex3.h5ad?download=1" | |
| DRUG_URL = "https://huggingface.co/datasets/tahoebio/Tahoe-100M/resolve/main/metadata/drug_metadata.parquet" | |
| def read_column(node): | |
| if isinstance(node, h5py.Group): | |
| categories = read_column(node["categories"]) | |
| codes = node["codes"][:] | |
| out = categories[np.maximum(codes, 0)].astype(object) | |
| out[codes < 0] = None | |
| return out | |
| values = node[:] | |
| return np.array([v.decode() if isinstance(v, bytes) else v for v in values]) if values.dtype.kind in "SO" else values | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--output", required=True) | |
| parser.add_argument("--source", default=URL, help="Local corrected h5ad or source URL") | |
| parser.add_argument("--cells-per-condition", type=int, default=8) | |
| parser.add_argument("--cell-line", default="A549") | |
| parser.add_argument("--max-molecules", type=int, default=40) | |
| parser.add_argument("--seed", type=int, default=0) | |
| parser.add_argument("--metadata-cache", help="Optional locally generated metadata cache directory") | |
| args = parser.parse_args() | |
| output = Path(args.output); output.mkdir(parents=True, exist_ok=True) | |
| cache = Path(args.metadata_cache) if args.metadata_cache else output | |
| drug_file = cache/"drug_metadata.parquet" | |
| if not drug_file.exists(): | |
| response = requests.get(DRUG_URL, timeout=60); response.raise_for_status() | |
| drug_file.parent.mkdir(parents=True, exist_ok=True); drug_file.write_bytes(response.content) | |
| # Arrow conversion avoids dependence on a pandas-specific parquet extension registry. | |
| drugs = pd.DataFrame(pq.read_table(drug_file).to_pylist()) | |
| lookup = {s.strip().casefold(): i for i, s in enumerate(drugs.drug) if isinstance(s, str)} | |
| rng = np.random.default_rng(args.seed) | |
| start = time.perf_counter() | |
| handle = RangeReader(args.source, output/'range_cache', 2526631614) if args.source.startswith('https://') else open(args.source, 'rb') | |
| with handle: | |
| with h5py.File(handle, "r") as h: | |
| names = ["cell_line","dose_value","perturbation","replicate","plate","well","time"] | |
| obs = pd.DataFrame({name: read_column(h["obs"][name]) for name in names}) | |
| print(f"Loaded metadata for {len(obs)} cells", flush=True) | |
| matches = sorted(s for s in obs.perturbation.dropna().unique() if s.strip().casefold() in lookup)[:args.max_molecules] | |
| mappings = [{"drug": s, "smiles": drugs.iloc[lookup[s.strip().casefold()]].canonical_smiles, | |
| "tahoe_name": drugs.iloc[lookup[s.strip().casefold()]].drug, | |
| "matching_rule": "exact case-folded name after outer whitespace removal"} for s in matches] | |
| pd.DataFrame(mappings).to_csv(output/"structures.csv", index=False) | |
| eligible = obs[(obs.cell_line == args.cell_line) & (obs.time == 24.)] | |
| treated = eligible[eligible.perturbation.isin(matches) & eligible.dose_value.isin([100.,1000.])] | |
| selected = [] | |
| for _, group in treated.groupby(["perturbation","dose_value","replicate","plate"], sort=True): | |
| if len(group) >= args.cells_per_condition: | |
| selected.extend(rng.choice(group.index, args.cells_per_condition, replace=False)) | |
| plates = set(obs.loc[selected, "plate"]) | |
| controls = eligible[(eligible.perturbation == "control") & eligible.plate.isin(plates)] | |
| for _, group in controls.groupby(["replicate","plate"], sort=True): | |
| selected.extend(rng.choice(group.index, min(args.cells_per_condition*2,len(group)), replace=False)) | |
| selected = np.sort(np.unique(selected)) | |
| x = h["X"] | |
| if x.attrs.get("encoding-type") != "csr_matrix": raise ValueError("Expected source CSR matrix") | |
| pointers = x["indptr"][:] | |
| values, columns, pointer = [], [], [0] | |
| for j, index in enumerate(selected): | |
| begin, end = int(pointers[index]), int(pointers[index+1]) | |
| values.append(x["data"][begin:end]); columns.append(x["indices"][begin:end]) | |
| pointer.append(pointer[-1]+end-begin) | |
| if j % 100 == 0: print(f"Read {j}/{len(selected)} selected cell rows", flush=True) | |
| matrix = sparse.csr_matrix((np.concatenate(values),np.concatenate(columns),np.asarray(pointer)), | |
| shape=(len(selected),int(x.attrs["shape"][1]))) | |
| var_index = h["var"].attrs.get("_index", "_index") | |
| if isinstance(var_index, bytes): var_index=var_index.decode() | |
| gene_names = read_column(h["var"][var_index]).astype(str) | |
| subset_obs = obs.iloc[selected].copy() | |
| subset_obs["source_row"] = selected | |
| subset_obs.index = [f"sciplex3_row_{i}" for i in selected] | |
| result = ad.AnnData(matrix, obs=subset_obs, var=pd.DataFrame(index=gene_names)) | |
| result.write_h5ad(output/"sciplex_prototype.h5ad", compression="gzip") | |
| manifest = {"source_url":URL,"source_record":"scPerturb corrected v1.4 / Zenodo 13350497", | |
| "source_md5":"c9e70629505d98c7ca1a837f62b14e89", "full_source_checksum_recomputed":False, | |
| "smiles_source":DRUG_URL,"molecules":len(matches),"cells":len(selected), | |
| "gene_columns":matrix.shape[1],"nonzero_counts":matrix.nnz,"cell_line":args.cell_line, | |
| "exposure_hours":24,"dose_um":[.1,1.],"seed":args.seed, | |
| "prototype_only":True,"elapsed_seconds":time.perf_counter()-start, | |
| "selected_rows_sha256":hashlib.sha256(selected.tobytes()).hexdigest()} | |
| (output/"source_manifest.json").write_text(json.dumps(manifest, indent=2)+"\n") | |
| print(json.dumps(manifest,indent=2)) | |
| if __name__ == "__main__": main() | |