| |
| """ |
| Download the RaSP proteome-wide ΞΞG dataset (Blaabjerg et al., eLife 2023) |
| and upload it as a partitioned HF dataset table (ddg/). |
| |
| Source: https://sid.erda.dk/sharelink/fFPJWflLeE |
| File: rasp_preds_alphafold_UP000005640_9606_HUMAN_v2_vaex_dataframe.zip |
| ~230M variants, 23 391 human proteins, AlphaFold2 structures, kcal/mol |
| |
| Strategy |
| -------- |
| 1. Download the Vaex bulk file from ERDA (~8.6 GB zip) |
| 2. Convert Vaex (HDF5) -> flat Parquet using vaex |
| 3. DuckDB COPY partitions by protein_id (UniProt extracted from pdbid) |
| 4. upload_large_folder -> ddg/ on HF (same pattern as esm1b/) |
| 5. Backend JOIN: COALESCE(variants.pred_ddg, ddg.pred_ddg) |
| -> ESM-IF1 values kept for 13 enriched proteins, RaSP for all others |
| |
| Column mapping |
| -------------- |
| RaSP column -> MutVar column |
| pdbid -> protein_id (AF-{UNIPROT}-F1-... -> extract UniProt) |
| variant -> mutation_code (e.g. "V600E" β exact match) |
| score_ml -> pred_ddg (kcal/mol, positive = destabilising) |
| |
| Threshold: pred_ddg > 1.5 kcal/mol -> destabilising (same as ESM-IF1 proxy) |
| |
| Estimated runtime |
| ----------------- |
| Download : ~30-60 min (8.6 GB from ERDA) |
| Vaex convert : ~10-20 min (HDF5 -> flat parquet) |
| DuckDB COPY : ~10-20 min (partition 230M rows) |
| upload_large : ~1-3h (chunked, resumable) |
| Total : ~3-5h |
| |
| Resume behaviour |
| ---------------- |
| Output dir defaults to ~/mutvar_rasp_upload/ |
| Partition step is skipped if ddg/ subfolder already has parquet files. |
| Upload is resumable (upload_large_folder uses .cache/ for state). |
| |
| Usage |
| ----- |
| pip install h5py duckdb huggingface_hub pyarrow |
| huggingface-cli login |
| python scripts/join_rasp_ddg.py [--dry-run] [--output-dir PATH] |
| """ |
|
|
| import os, re, argparse, time, zipfile |
| from pathlib import Path |
| import duckdb |
|
|
| HF_DATASET = os.environ.get("HF_DATASET", "edohollou/mutvar-variants") |
| ERDA_BASE = "https://sid.erda.dk/share_redirect/fFPJWflLeE" |
| VAEX_FILENAME = "rasp_preds_alphafold_UP000005640_9606_HUMAN_v2_vaex_dataframe.zip" |
| DDG_THRESHOLD = 1.5 |
|
|
| DEFAULT_OUT_DIR = Path.home() / "mutvar_rasp_upload" |
|
|
|
|
| |
|
|
| def download_vaex(out_dir: Path) -> Path: |
| """Download the Vaex zip from ERDA if not already present.""" |
| zip_path = out_dir / VAEX_FILENAME |
| if zip_path.exists(): |
| print(f"[1/4] Vaex zip already downloaded: {zip_path}") |
| return zip_path |
|
|
| import urllib.request |
| url = f"{ERDA_BASE}/{VAEX_FILENAME}" |
| out_dir.mkdir(parents=True, exist_ok=True) |
| print(f"[1/4] Downloading RaSP Vaex file (~8.6 GB)...") |
| print(f" {url}") |
| print(f" -> {zip_path}") |
|
|
| t0 = time.time() |
|
|
| def _progress(count, block, total): |
| pct = min(100, count * block * 100 // total) |
| done = count * block / 1e9 |
| tot = total / 1e9 |
| print(f"\r {done:.2f} / {tot:.2f} GB ({pct}%)", end="", flush=True) |
|
|
| urllib.request.urlretrieve(url, zip_path, reporthook=_progress) |
| print(f"\n Done in {(time.time()-t0)/60:.1f} min") |
| return zip_path |
|
|
|
|
| |
|
|
| def _hdf5_is_valid(path: Path) -> bool: |
| """Quick integrity check β opens the HDF5 and reads the row count.""" |
| try: |
| import h5py |
| with h5py.File(str(path), "r") as f: |
| for col_path in [ |
| "table/columns/pdbid/data", |
| "columns/pdbid/data", |
| "pdbid", |
| ]: |
| if col_path in f: |
| _ = len(f[col_path]) |
| return True |
| return False |
| except Exception: |
| return False |
|
|
|
|
| def vaex_to_parquet(zip_path: Path, flat_parquet: Path) -> Path: |
| """ |
| Unzip the Vaex HDF5 and convert to a flat Parquet using h5py. |
| Reads in chunks of 5M rows to avoid OOM. |
| |
| Space management: |
| - Skips extraction if a valid HDF5 already exists |
| - Deletes the zip immediately after successful extraction |
| - Deletes the HDF5 immediately after the parquet is written |
| """ |
| if flat_parquet.exists(): |
| size_gb = flat_parquet.stat().st_size / 1e9 |
| print(f"[2/4] Flat parquet already exists ({size_gb:.2f} GB): {flat_parquet}") |
| return flat_parquet |
|
|
| import h5py |
| import pyarrow as pa |
| import pyarrow.parquet as pq |
|
|
| |
| |
| with zipfile.ZipFile(zip_path, "r") as zf: |
| names = zf.namelist() |
| hdf5_files = [n for n in names if ".hdf5" in n or n.endswith(".h5")] |
| if not hdf5_files: |
| raise RuntimeError(f"No HDF5 in zip. Contents: {names[:20]}") |
| hdf5_name = hdf5_files[0] |
|
|
| hdf5_path = flat_parquet.parent / Path(hdf5_name).name |
|
|
| if hdf5_path.exists() and _hdf5_is_valid(hdf5_path): |
| print(f"[2/4] HDF5 already extracted and valid β skipping unzip.") |
| print(f" {hdf5_path}") |
| else: |
| |
| if hdf5_path.exists(): |
| print(f" Removing corrupt/partial HDF5 ({hdf5_path.stat().st_size/1e9:.1f} GB)...") |
| hdf5_path.unlink() |
|
|
| print("[2/4] Extracting HDF5 from zip...") |
| t0 = time.time() |
| with zipfile.ZipFile(zip_path, "r") as zf: |
| zf.extract(hdf5_name, flat_parquet.parent) |
| print(f" Extracted in {(time.time()-t0)/60:.1f} min " |
| f"({hdf5_path.stat().st_size/1e9:.1f} GB)") |
|
|
| |
| try: |
| zip_path.unlink() |
| print(f" Deleted zip to reclaim {zip_path.stat().st_size/1e9:.1f} GB") |
| except Exception: |
| pass |
|
|
| |
| print("[2/4] Converting HDF5 -> flat Parquet (chunked, h5py)...") |
| t0 = time.time() |
|
|
| _uid_re = re.compile(r"AF-([A-Z0-9]+)-F\d") |
| CHUNK = 5_000_000 |
|
|
| with h5py.File(str(hdf5_path), "r") as f: |
| print(f" HDF5 top-level keys: {list(f.keys())}") |
|
|
| def _open_col(name: str): |
| for path in [ |
| f"table/columns/{name}/data", |
| f"columns/{name}/data", |
| name, |
| ]: |
| if path in f: |
| return f[path] |
| raise KeyError(f"Column '{name}' not found. Available: {list(f.keys())}") |
|
|
| col_pdbid = _open_col("pdbid") |
| col_variant = _open_col("variant") |
| col_score = _open_col("score_ml") |
| n_total = len(col_pdbid) |
| print(f" {n_total:,} rows total") |
|
|
| writer = None |
| for start in range(0, n_total, CHUNK): |
| end = min(start + CHUNK, n_total) |
|
|
| pdbids = col_pdbid[start:end].astype(str) |
| variants = col_variant[start:end].astype(str) |
| scores = col_score[start:end].astype("float32") |
|
|
| protein_ids = [ |
| (m.group(1) if (m := _uid_re.search(p)) else p) |
| for p in pdbids |
| ] |
|
|
| chunk = pa.table({ |
| "protein_id": pa.array(protein_ids, type=pa.string()), |
| "mutation_code": pa.array(variants.tolist(), type=pa.string()), |
| "pred_ddg": pa.array(scores.tolist(), type=pa.float32()), |
| "pred_ddg_label": pa.array( |
| (scores > DDG_THRESHOLD).tolist(), type=pa.bool_() |
| ), |
| }) |
|
|
| if writer is None: |
| writer = pq.ParquetWriter(str(flat_parquet), chunk.schema) |
| writer.write_table(chunk) |
|
|
| pct = end * 100 // n_total |
| print(f"\r {end:,} / {n_total:,} rows ({pct}%)", end="", flush=True) |
|
|
| if writer: |
| writer.close() |
| print() |
|
|
| |
| try: |
| size = hdf5_path.stat().st_size / 1e9 |
| hdf5_path.unlink() |
| print(f" Deleted HDF5 ({size:.1f} GB freed)") |
| except Exception: |
| pass |
|
|
| size_gb = flat_parquet.stat().st_size / 1e9 |
| print(f" Flat parquet: {size_gb:.2f} GB in {(time.time()-t0)/60:.1f} min") |
| return flat_parquet |
|
|
|
|
| |
|
|
| def partition_locally(flat_parquet: Path, ddg_dir: Path, dry_run: bool) -> int: |
| """ |
| DuckDB reads the flat parquet and writes per-protein partitions. |
| Skipped if ddg_dir already contains parquet files. |
| """ |
| existing = list(ddg_dir.rglob("*.parquet")) |
| if existing and not dry_run: |
| print(f"[3/4] Partition dir already exists ({len(existing):,} files) β skipping.") |
| return len(existing) |
|
|
| con = duckdb.connect() |
| ddg_dir.mkdir(parents=True, exist_ok=True) |
|
|
| if dry_run: |
| print("[3/4] [dry-run] Partitioning 5-protein sample...") |
| sample = con.execute(f""" |
| SELECT DISTINCT protein_id FROM read_parquet('{flat_parquet}') |
| ORDER BY protein_id LIMIT 5 |
| """).fetchall() |
| sample_ids = [r[0] for r in sample] |
| pid_list = ", ".join(f"'{p}'" for p in sample_ids) |
| print(f" Sample: {sample_ids}") |
|
|
| con.execute(f""" |
| COPY ( |
| SELECT protein_id, mutation_code, |
| ROUND(pred_ddg, 4) AS pred_ddg, |
| pred_ddg_label |
| FROM read_parquet('{flat_parquet}') |
| WHERE protein_id IN ({pid_list}) |
| ) |
| TO '{ddg_dir}' |
| (FORMAT PARQUET, PARTITION_BY (protein_id), OVERWRITE_OR_IGNORE true) |
| """) |
| else: |
| print("[3/4] Partitioning with DuckDB (~10-20 min)...") |
| t0 = time.time() |
| con.execute(f""" |
| COPY ( |
| SELECT protein_id, mutation_code, |
| ROUND(pred_ddg, 4) AS pred_ddg, |
| pred_ddg_label |
| FROM read_parquet('{flat_parquet}') |
| ) |
| TO '{ddg_dir}' |
| (FORMAT PARQUET, PARTITION_BY (protein_id), OVERWRITE_OR_IGNORE true) |
| """) |
| print(f" Done in {(time.time()-t0)/60:.1f} min") |
|
|
| n = len(list(ddg_dir.rglob("*.parquet"))) |
| print(f" {n:,} parquet files in {ddg_dir}") |
| con.close() |
| return n |
|
|
|
|
| |
|
|
| def upload(upload_root: Path, dry_run: bool): |
| if dry_run: |
| print("[4/4] [dry-run] Skipping upload. Sample files:") |
| import pandas as pd |
| ddg_dir = upload_root / "ddg" |
| for f in sorted(ddg_dir.rglob("*.parquet"))[:5]: |
| df = pd.read_parquet(f) |
| print(f" {f.parent.name}/{f.name}: {len(df)} rows, " |
| f"pred_ddg sample={df['pred_ddg'].head(3).tolist()}") |
| return |
|
|
| from huggingface_hub import HfApi |
| api = HfApi() |
| print("[4/4] Uploading ddg/ to HuggingFace (upload_large_folder, resumable)...") |
| print(" Safe to Ctrl-C and re-run β upload resumes from last checkpoint.") |
| t0 = time.time() |
|
|
| api.upload_large_folder( |
| folder_path=str(upload_root), |
| repo_id=HF_DATASET, |
| repo_type="dataset", |
| ) |
|
|
| print(f" Done in {(time.time()-t0)/60:.1f} min") |
| print() |
| print("[done] Next steps:") |
| print(" 1. git push -> Space picks up backend/main.py JOIN for ddg/") |
| print(f" 2. Delete local files: rm -rf {upload_root}") |
|
|
|
|
| |
|
|
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--dry-run", action="store_true", |
| help="Process 5 proteins only, skip upload") |
| parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUT_DIR, |
| help=f"Persistent output dir (default: {DEFAULT_OUT_DIR})") |
| args = parser.parse_args() |
|
|
| upload_root = args.output_dir |
| ddg_dir = upload_root / "ddg" |
| flat_parquet = upload_root / "rasp_flat.parquet" |
|
|
| print(f" Upload root : {upload_root}") |
| print(f" Parquet dir : {ddg_dir}\n") |
|
|
| zip_path = download_vaex(upload_root) |
| vaex_to_parquet(zip_path, flat_parquet) |
| n = partition_locally(flat_parquet, ddg_dir, dry_run=args.dry_run) |
| if n > 0: |
| upload(upload_root, dry_run=args.dry_run) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|