Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python | |
| """Prepare ClinVar GRCh38 SNV sequence windows for DNABERT-2 fine-tuning. | |
| This script is intended for Google Colab. It expects a ClinVar GRCh38 VCF and | |
| a GRCh38 FASTA indexed by pysam. The output is JSONL, one example per variant. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import gzip | |
| import json | |
| from pathlib import Path | |
| from typing import Iterable | |
| import pysam | |
| LABEL_TO_ID = { | |
| "likely_benign": 0, | |
| "likely_pathogenic": 1, | |
| } | |
| SKIP_CLNSIG_TERMS = ( | |
| "conflicting", | |
| "uncertain", | |
| "not provided", | |
| "not_provided", | |
| "other", | |
| "risk factor", | |
| "association", | |
| "drug response", | |
| "protective", | |
| ) | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--clinvar-vcf", required=True, help="Path to ClinVar GRCh38 VCF or VCF.GZ.") | |
| parser.add_argument("--reference-fasta", required=True, help="Path to indexed GRCh38 FASTA.") | |
| parser.add_argument("--output-jsonl", required=True, help="Destination JSONL file.") | |
| parser.add_argument("--window-size", type=int, default=251, help="Odd sequence window size centered on the variant.") | |
| parser.add_argument("--max-records", type=int, default=0, help="Optional cap for quick Colab smoke tests. 0 means no cap.") | |
| return parser.parse_args() | |
| def open_text(path: str): | |
| if path.endswith(".gz"): | |
| return gzip.open(path, "rt") | |
| return open(path, "rt", encoding="utf-8") | |
| def parse_info(info: str) -> dict[str, str]: | |
| parsed: dict[str, str] = {} | |
| for item in info.split(";"): | |
| if not item: | |
| continue | |
| if "=" not in item: | |
| parsed[item] = "true" | |
| continue | |
| key, value = item.split("=", 1) | |
| parsed[key] = value | |
| return parsed | |
| def normalize_clnsig(raw: str) -> str: | |
| return ( | |
| raw.replace("%2C", ",") | |
| .replace("%2c", ",") | |
| .replace("_", " ") | |
| .replace("/", " ") | |
| .replace("|", " ") | |
| .lower() | |
| ) | |
| def map_clnsig(raw: str | None) -> str | None: | |
| if not raw: | |
| return None | |
| normalized = normalize_clnsig(raw) | |
| if any(term in normalized for term in SKIP_CLNSIG_TERMS): | |
| return None | |
| has_pathogenic = "pathogenic" in normalized | |
| has_benign = "benign" in normalized | |
| if has_pathogenic and has_benign: | |
| return None | |
| if has_pathogenic: | |
| return "likely_pathogenic" | |
| if has_benign: | |
| return "likely_benign" | |
| return None | |
| def extract_gene(info: dict[str, str]) -> str | None: | |
| gene_info = info.get("GENEINFO") | |
| if not gene_info: | |
| return None | |
| first_gene = gene_info.split("|", 1)[0] | |
| return first_gene.split(":", 1)[0] or None | |
| def contig_candidates(chrom: str) -> Iterable[str]: | |
| yield chrom | |
| if chrom.startswith("chr"): | |
| yield chrom.removeprefix("chr") | |
| else: | |
| yield f"chr{chrom}" | |
| if chrom == "MT": | |
| yield "chrM" | |
| if chrom == "chrM": | |
| yield "MT" | |
| def fetch_window(reference: pysam.FastaFile, chrom: str, pos: int, window_size: int) -> tuple[str, str] | None: | |
| half = window_size // 2 | |
| start = pos - 1 - half | |
| end = pos - 1 + half + 1 | |
| if start < 0: | |
| return None | |
| for contig in contig_candidates(chrom): | |
| if contig not in reference.references: | |
| continue | |
| if end > reference.get_reference_length(contig): | |
| return None | |
| return contig, reference.fetch(contig, start, end).upper() | |
| return None | |
| def make_alt_sequence(reference_sequence: str, alt: str) -> str: | |
| center = len(reference_sequence) // 2 | |
| return reference_sequence[:center] + alt.upper() + reference_sequence[center + 1 :] | |
| def prepare_examples(args: argparse.Namespace) -> tuple[int, int]: | |
| output_path = Path(args.output_jsonl) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| if args.window_size % 2 == 0: | |
| raise ValueError("--window-size must be odd so the variant has a center base.") | |
| written = 0 | |
| scanned = 0 | |
| reference = pysam.FastaFile(args.reference_fasta) | |
| with open_text(args.clinvar_vcf) as vcf, output_path.open("w", encoding="utf-8") as output: | |
| for line in vcf: | |
| if line.startswith("#"): | |
| continue | |
| scanned += 1 | |
| fields = line.rstrip("\n").split("\t") | |
| if len(fields) < 8: | |
| continue | |
| chrom, pos_raw, variant_id, ref, alts, _qual, _filter, info_raw = fields[:8] | |
| pos = int(pos_raw) | |
| info = parse_info(info_raw) | |
| label = map_clnsig(info.get("CLNSIG")) | |
| if label is None: | |
| continue | |
| ref = ref.upper() | |
| for alt in alts.split(","): | |
| alt = alt.upper() | |
| if len(ref) != 1 or len(alt) != 1: | |
| continue | |
| if ref not in {"A", "C", "G", "T"} or alt not in {"A", "C", "G", "T"}: | |
| continue | |
| fetched = fetch_window(reference, chrom, pos, args.window_size) | |
| if fetched is None: | |
| continue | |
| resolved_contig, reference_sequence = fetched | |
| if len(reference_sequence) != args.window_size: | |
| continue | |
| center_base = reference_sequence[len(reference_sequence) // 2] | |
| if center_base != ref: | |
| continue | |
| example = { | |
| "id": variant_id, | |
| "chromosome": resolved_contig, | |
| "position": pos, | |
| "reference": ref, | |
| "alternate": alt, | |
| "gene": extract_gene(info), | |
| "clnsig": info.get("CLNSIG"), | |
| "sequence": make_alt_sequence(reference_sequence, alt), | |
| "reference_sequence": reference_sequence, | |
| "label": label, | |
| "label_id": LABEL_TO_ID[label], | |
| "grch_build": "GRCh38", | |
| } | |
| output.write(json.dumps(example) + "\n") | |
| written += 1 | |
| if args.max_records and written >= args.max_records: | |
| return scanned, written | |
| return scanned, written | |
| def main() -> None: | |
| args = parse_args() | |
| scanned, written = prepare_examples(args) | |
| print(f"Scanned {scanned:,} ClinVar records.") | |
| print(f"Wrote {written:,} GRCh38 SNV examples to {args.output_jsonl}.") | |
| if __name__ == "__main__": | |
| main() | |