aa5669fe641fb39d7711eb81ec05092d14f267fb lrnassar Tue Jun 30 15:20:56 2026 -0700 Adding Cardiomyopathy VCEP track hub build scripts and makedoc. refs #37446 Adds the 12 per-track build scripts under src/hg/makeDb/scripts/cardiomyopathyVCEP/ (gnomAD v4.1 allele frequencies, REVEL, CardioBoost, the hgVai consequence/HGVSp annotation layer, ClinGen EvRepo, ClinVar submitter 506161, Walsh 2019 curations, PM1 clinical-domain hotspots, MYBPC3 PVS1 caveats, Walsh 2017 PS4 odds-ratio track, Atlas PS4 per-variant OR, and the NON-FINAL provisional classifier) plus the build documentation at src/hg/makeDb/doc/Cardiomyopathy.txt. All ACMG thresholds are taken directly from the ClinGen Cardiomyopathy CSpecs (8 genes, affiliation 50002); no thresholds are invented in the build. diff --git src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPClinVar506161.py src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPClinVar506161.py new file mode 100644 index 00000000000..116125b3ef9 --- /dev/null +++ src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPClinVar506161.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +""" +B.8 — Cardiomyopathy VCEP ClinVar submitter 506161 track builder. + +Renders all 199 ClinVar submissions from "ClinGen Cardiomyopathy Variant Curation +Expert Panel" — verified at A.3 to be 100% "reviewed by expert panel" (no relabel +branch needed). + +Input: + /hive/users/lrnassar/claude/RM37446/cmp_downloads/clinvar/cardiomyopathyVCEP_submissions.tsv + (199 submission rows, schema = ClinVar submission_summary.txt.gz) + /hive/data/outside/otto/clinvar/downloads/2026-05-30/variant_summary.txt.gz + (per-VariationID hg19 + hg38 coords; joined by col 31 = VariationID) + +Outputs: + cmpVCEPClinVar506161/cmpVCEPClinVar506161.as + cmpVCEPClinVar506161/cmpVCEPClinVar506161Hg38.bed + .bb + cmpVCEPClinVar506161/cmpVCEPClinVar506161Hg19.bed + .bb + +Usage: + python3 cmpVCEPClinVar506161.py --db hg38 --db hg19 \ + --output-dir /hive/users/lrnassar/claude/RM37446 +""" + +import argparse, gzip, json, os, subprocess, sys + +SUBMISSIONS_TSV = '/hive/users/lrnassar/claude/RM37446/cmp_downloads/clinvar/cardiomyopathyVCEP_submissions.tsv' +VARIANT_SUMMARY = '/hive/data/outside/otto/clinvar/downloads/2026-05-30/variant_summary.txt.gz' + +# Standard 5-tier ACMG ramp (same as EvRepo / Walsh 2019); the "with codes vs no codes" +# distinction is carried by the track name and mouseover, not by color. +COLORS = { + 'Pathogenic': '210,0,0', + 'Likely pathogenic': '245,152,152', + 'Uncertain significance': '0,0,136', + 'Likely benign': '213,247,213', + 'Benign': '0,210,0', +} +DEFAULT_COLOR = '136,136,136' + +# Per-gene EvRepo VariationIDs — built once at build-time from EvRepo JSON +EVREPO_VAR_IDS = set() +EVREPO_JSON = '/hive/users/lrnassar/claude/RM37446/cmp_downloads/erepo/cardiomyopathyVCEP_classifications.json' +try: + _erepo = json.load(open(EVREPO_JSON)) + EVREPO_VAR_IDS = set(v.get('variationId', '') for v in _erepo['variantInterpretations'] if v.get('variationId')) + print(f' loaded {len(EVREPO_VAR_IDS)} EvRepo VariationIDs (will tag overlapping rows)') +except Exception as e: + print(f' WARNING: could not load EvRepo JSON: {e}', file=sys.stderr) + +CHROM_SIZES = { + 'hg38': '/cluster/data/hg38/chrom.sizes', + 'hg19': '/cluster/data/hg19/chrom.sizes', +} + +AUTOSQL = """table cmpVCEPClinVar506161 +"ClinVar submitter 506161 (ClinGen Cardiomyopathy VCEP) - Final, no evidence codes attached" + ( + string chrom; "Chromosome" + uint chromStart; "Start position (BED 0-based)" + uint chromEnd; "End position (BED half-open)" + string name; "Display name (gene + hgvsP + classification initial)" + uint score; "Always 0" + char[1] strand; "Always +" + uint thickStart; "Same as chromStart" + uint thickEnd; "Same as chromEnd" + uint itemRgb; "Display color (desaturated ACMG palette)" + string gene; "Gene symbol" + string hgvsName; "Canonical HGVS Name (NM_xx(GENE):c.x>y (p.xxx))" + string classification; "VCEP classification" + string reviewStatus; "ClinVar review status (verified all-expert-panel at A.3)" + string lastEvaluated; "Date last evaluated" + string scvAccession; "SCV submission accession" + string variationId; "ClinVar VariationID" + string rcvAccession; "ClinVar RCV accession" + string inEvRepo; "yes if also in EvRepo (variant is in both sources)" + string clinvarUrl; "ClinVar variant URL" + lstring _mouseOver; "Tooltip HTML" + ) +""" + + +def load_submissions(): + """Parse 199-row TSV. Returns dict by VariationID (string) → submission record.""" + submissions = {} + header = None + for line in open(SUBMISSIONS_TSV): + line = line.rstrip('\n') + if line.startswith('##'): + continue + if line.startswith('#'): + header = line.lstrip('#').split('\t') + continue + f = line.split('\t') + if len(f) < 12: + continue + var_id = f[0] + submissions[var_id] = { + 'classification': f[1], + 'last_evaluated': f[2], + 'description': f[3], + 'review_status': f[6], + 'submitter': f[9], + 'scv': f[10], + 'gene': f[11] if len(f) > 11 else '', + } + print(f' loaded {len(submissions)} ClinVar submitter-506161 records') + return submissions + + +def load_variant_summary_coords(submissions): + """Stream variant_summary.txt.gz, picking rows whose VariationID (col 31) is in our set. + Returns dict: var_id → {assembly: {chrom, start_bed, end_bed, ref, alt, hgvs_name, rcv}}.""" + var_ids = set(submissions.keys()) + coords = {vid: {} for vid in var_ids} + + # Schema (1-based positions in awk; 0-based here): + # 3=Name, 5=GeneSymbol, 12=RCVaccession, 17=Assembly, 18=ChromosomeAccession, + # 19=Chromosome, 20=Start, 21=Stop, 22=ReferenceAllele, 23=AlternateAllele, 31=VariationID + n_rows_seen = 0 + with gzip.open(VARIANT_SUMMARY, 'rt') as fh: + for line in fh: + if line.startswith('#'): + continue + f = line.rstrip('\n').split('\t') + if len(f) < 31: + continue + var_id = f[30] # VariationID (1-based col 31 → 0-based index 30) + if var_id not in var_ids: + continue + n_rows_seen += 1 + assembly = f[16] # GRCh38 / GRCh37 + chrom_num = f[18] # 1, 2, ..., X + try: + start1 = int(f[19]) # 1-based start + stop1 = int(f[20]) # 1-based stop (inclusive) + except ValueError: + continue + ref = f[21] + alt = f[22] + chrom = f'chr{chrom_num}' + db = 'hg38' if assembly == 'GRCh38' else ('hg19' if assembly == 'GRCh37' else None) + if db is None: + continue + # BED is 0-based half-open + start_bed = start1 - 1 + end_bed = stop1 + coords[var_id][db] = { + 'chrom': chrom, + 'start': start_bed, + 'end': end_bed, + 'ref': ref, + 'alt': alt, + 'hgvs_name': f[2], + 'gene': f[4], + 'rcv': f[11], + } + print(f' matched {n_rows_seen} variant_summary rows for {len(var_ids)} VariationIDs') + return coords + + +def emit_bed(submissions, coords, db, out_path): + rows = [] + skipped = 0 + for var_id, sub in submissions.items(): + c = coords.get(var_id, {}).get(db) + if c is None: + skipped += 1 + continue + # Extract HGVS protein from Name (NM_xxx(GENE):c.xxx (p.yyy)) + hgvs_name = c['hgvs_name'] + protein_short = '' + if '(p.' in hgvs_name: + try: + protein_short = hgvs_name.split('(p.')[1].rstrip(')') + except IndexError: + pass + if not protein_short: + # fallback to first 12 chars of HGVS Name after the colon + protein_short = hgvs_name.split(':', 1)[-1][:20].replace(' ', '_') + + gene = c['gene'] or sub['gene'] + cls = sub['classification'] + color = COLORS.get(cls, DEFAULT_COLOR) + in_evrepo = 'yes' if var_id in EVREPO_VAR_IDS else 'no' + clinvar_url = f'https://www.ncbi.nlm.nih.gov/clinvar/variation/{var_id}/' + + name = f'{gene}_{protein_short}_{cls.split()[0][:1]}' + mouseover = ( + f'Final - ClinVar submitter 506161 (no evidence codes)
' + f'{gene} {hgvs_name}
' + f'Classification: {cls}
' + f'Review: {sub["review_status"]}
' + f'Last evaluated: {sub["last_evaluated"]}
' + f'In EvRepo: {in_evrepo}' + ) + + rows.append('\t'.join([ + c['chrom'], str(c['start']), str(c['end']), + name, '0', '+', + str(c['start']), str(c['end']), color, + gene, + hgvs_name, + cls, + sub['review_status'], + sub['last_evaluated'], + sub['scv'], + var_id, + c['rcv'], + in_evrepo, + clinvar_url, + mouseover, + ])) + + rows.sort(key=lambda l: (l.split('\t')[0], int(l.split('\t')[1]))) + with open(out_path, 'w') as f: + for line in rows: + f.write(line + '\n') + print(f' wrote {len(rows)} BED features → {out_path} (skipped {skipped})') + return len(rows) + + +def make_bigbed(bed_path, db, as_path, bb_path): + cmd = ['bedToBigBed', '-tab', '-type=bed9+11', '-as=' + as_path, + bed_path, CHROM_SIZES[db], bb_path] + print(f' $ {" ".join(cmd)}') + subprocess.run(cmd, check=True) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--db', action='append', required=True, choices=['hg38', 'hg19']) + ap.add_argument('--output-dir', required=True) + args = ap.parse_args() + + out_dir = os.path.join(args.output_dir, 'cmpVCEPClinVar506161') + os.makedirs(out_dir, exist_ok=True) + + print(' [B.8 ClinVar submitter 506161]') + + submissions = load_submissions() + coords = load_variant_summary_coords(submissions) + + # Distribution sanity + from collections import Counter + review = Counter(s['review_status'] for s in submissions.values()) + print(f' review-status: {dict(review)} (expecting 199 expert-panel-reviewed)') + classes = Counter(s['classification'] for s in submissions.values()) + print(f' classifications: {dict(classes)}') + n_in_erepo = sum(1 for v in submissions if v in EVREPO_VAR_IDS) + print(f' overlap with EvRepo: {n_in_erepo} of {len(submissions)}') + + as_path = os.path.join(out_dir, 'cmpVCEPClinVar506161.as') + with open(as_path, 'w') as f: + f.write(AUTOSQL) + + counts = {} + for db in args.db: + bed_path = os.path.join(out_dir, f'cmpVCEPClinVar506161Hg{"38" if db=="hg38" else "19"}.bed') + bb_path = os.path.join(out_dir, f'cmpVCEPClinVar506161Hg{"38" if db=="hg38" else "19"}.bb') + n = emit_bed(submissions, coords, db, bed_path) + make_bigbed(bed_path, db, as_path, bb_path) + counts[db] = n + print(f' {db} bigBed: {bb_path}') + + if 'hg38' in counts and 'hg19' in counts: + if counts['hg38'] == counts['hg19']: + print(f' cross-assembly parity OK: {counts["hg38"]} features each') + else: + print(f' WARNING: parity FAILED — hg38={counts["hg38"]} hg19={counts["hg19"]}', + file=sys.stderr) + + +if __name__ == '__main__': + main()