6577d5ee1319bbea85988c1c89179436b4a94edf
lrnassar
Tue Jul 14 11:27:59 2026 -0700
Address code-review feedback on the Cardiomyopathy VCEP build scripts. refs #37446
- cmpVCEPCardioBoost.py: add the standard --db/--output-dir CLI. It previously
hardcoded the working directory for both its input TSV and its output (unlike
the 11 sibling scripts, and contrary to the makedoc's documented interface);
the build loop's flags were silently ignored. Output is unchanged (31,236
variants per assembly).
- Decode leftover HTML entities (arrows, >=, <=, +/-, x) in print/stderr
diagnostics, comments, and docstrings across all scripts so build logs read
cleanly. The mouseOver / bigBed display strings intentionally keep their
entities.
- cmpVCEPWalsh2019.py: fix the stale docstring that described the
ClinVar-unmatched entries as "deferred" (they are mapped via the hgvsToVcf
fallback, item L) and drop the unverified "163 rows" count.
Per code-review feedback on commit aa5669fe64. No track data changed.
diff --git src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPCardioBoost.py src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPCardioBoost.py
index 545bec84d68..53b463733bc 100644
--- src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPCardioBoost.py
+++ src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPCardioBoost.py
@@ -1,136 +1,142 @@
#!/usr/bin/env python3
"""
-B.5 — CardioBoost disease-specific missense predictor track.
+B.5 - CardioBoost disease-specific missense predictor track.
CardioBoost (Zhang et al. 2021, Genome Medicine 13:31) is a cardiomyopathy/arrhythmia
disease-specific missense pathogenicity predictor, complementary to REVEL. We render the
-precomputed cardiomyopathy-model scores (cm_prediction.RData → cardioboost_8genes_hg19.tsv,
+precomputed cardiomyopathy-model scores (cm_prediction.RData -> cardioboost_8genes_hg19.tsv,
exported with /usr/bin/Rscript) for our 8 genes.
IMPORTANT: CardioBoost is NOT a CSpec-specified predictor (the CSpec uses REVEL for PP3/BP4).
-This track is informational/supplementary — it does NOT fire an ACMG code. Whether the VCEP
+This track is informational/supplementary - it does NOT fire an ACMG code. Whether the VCEP
wants it used is an open question (see Phase 7). The displayed CardioBoost class uses the
-tool's OWN published thresholds (>=0.9 pathogenic, <=0.1 benign), cited — not invented here.
+tool's OWN published thresholds (>=0.9 pathogenic, <=0.1 benign), cited - not invented here.
Source is hg19 (GRCh37); we build hg19 natively and liftOver hg19->hg38.
Outputs:
cmpVCEPCardioBoost/cmpVCEPCardioBoost.as
cmpVCEPCardioBoost/cmpVCEPCardioBoostHg{38,19}.bed + .bb
"""
-import os, subprocess, sys
+import argparse, os, subprocess, sys
-WORKDIR = '/hive/users/lrnassar/claude/RM37446'
-TSV = f'{WORKDIR}/cmp_downloads/cardioboost/cardioboost_8genes_hg19.tsv'
CHROM_SIZES = {'hg38': '/cluster/data/hg38/chrom.sizes', 'hg19': '/cluster/data/hg19/chrom.sizes'}
LIFTOVER_HG19_TO_HG38 = '/cluster/data/hg19/bed/liftOver/hg19ToHg38.over.chain.gz'
-# CardioBoost's OWN published classification thresholds (Zhang 2021) — cited, not invented.
+# CardioBoost's OWN published classification thresholds (Zhang 2021) - cited, not invented.
CB_PATH = 0.90
CB_BENIGN = 0.10
COLORS = {'Pathogenic': '210,0,0', 'Benign': '0,160,0', 'Indeterminate': '150,150,150'}
AUTOSQL = """table cmpVCEPCardioBoost
"CardioBoost disease-specific missense predictor (Zhang 2021) - informational, NOT a CSpec code"
(
string chrom; "Chromosome"
uint chromStart; "Position (BED 0-based)"
uint chromEnd; "End (BED half-open; +1 for SNV)"
string name; "Display name (gene + protein change)"
uint score; "0"
char[1] strand; "Strand"
uint thickStart; "Same as chromStart"
uint thickEnd; "Same as chromEnd"
uint itemRgb; "Color by CardioBoost class"
string gene; "Gene symbol"
char[1] refAllele; "Reference nucleotide"
char[1] altAllele; "Alternate nucleotide"
string hgvsc; "CardioBoost HGVS c. (Ensembl transcript)"
string hgvsp; "CardioBoost HGVS p. (Ensembl protein)"
double cbScore; "CardioBoost pathogenicity probability (0-1)"
string cbClass; "CardioBoost class at published thresholds (>=0.9 P, <=0.1 B)"
lstring _mouseOver; "Tooltip HTML"
)
"""
def cb_class(score):
if score >= CB_PATH:
return 'Pathogenic'
if score <= CB_BENIGN:
return 'Benign'
return 'Indeterminate'
-def build_hg19_bed():
+def build_hg19_bed(tsv):
rows = []
- with open(TSV) as fh:
+ with open(tsv) as fh:
header = fh.readline()
for line in fh:
f = line.rstrip('\n').split('\t')
if len(f) < 8:
continue
chrom_num, pos, ref, alt, gene, hgvsc, hgvsp, score_s = f[:8]
try:
pos = int(pos); score = float(score_s)
except ValueError:
continue
if len(ref) != 1 or len(alt) != 1: # CardioBoost is missense SNVs
continue
chrom = 'chr' + chrom_num
start, end = pos - 1, pos
cls = cb_class(score)
pshort = hgvsp.split(':')[-1] if ':' in hgvsp else hgvsp
name = f'{gene}_{pshort}_{ref}{alt}_{score:.2f}' # ref/alt keeps same-aa variants distinct
mouse = (f'CardioBoost - disease-specific missense predictor (Zhang 2021)
'
f'{gene} {chrom}:{pos} {ref}>{alt} {pshort}
'
f'CardioBoost probability: {score:.3f} (class {cls}; tool thresholds ≥{CB_PATH} P, ≤{CB_BENIGN} B)
'
f'Informational — NOT a CSpec-specified predictor; the CSpec uses REVEL for '
f'PP3/BP4. Does not fire an ACMG code.')
rows.append('\t'.join([chrom, str(start), str(end), name, '0', '+',
str(start), str(end), COLORS[cls], gene, ref, alt,
hgvsc, pshort, f'{score:.6f}', cls, mouse]))
rows.sort(key=lambda l: (l.split('\t')[0], int(l.split('\t')[1])))
return rows
def main():
- out_dir = os.path.join(WORKDIR, 'cmpVCEPCardioBoost')
+ 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, 'cmpVCEPCardioBoost')
os.makedirs(out_dir, exist_ok=True)
+ tsv = os.path.join(args.output_dir, 'cmp_downloads/cardioboost/cardioboost_8genes_hg19.tsv')
print(' [B.5 CardioBoost predictor]')
as_path = os.path.join(out_dir, 'cmpVCEPCardioBoost.as')
with open(as_path, 'w') as f:
f.write(AUTOSQL)
- hg19_rows = build_hg19_bed()
+ # hg19 bed is always built: it is the native assembly and the liftOver source for hg38.
+ hg19_rows = build_hg19_bed(tsv)
hg19_bed = os.path.join(out_dir, 'cmpVCEPCardioBoostHg19.bed')
with open(hg19_bed, 'w') as f:
f.write('\n'.join(hg19_rows) + '\n')
print(f' hg19 (native): {len(hg19_rows)} variants')
+ if 'hg19' in args.db:
hg19_bb = os.path.join(out_dir, 'cmpVCEPCardioBoostHg19.bb')
subprocess.run(['bedToBigBed', '-tab', '-type=bed9+8', '-as=' + as_path,
hg19_bed, CHROM_SIZES['hg19'], hg19_bb], check=True)
+ if 'hg38' in args.db:
# liftOver hg19 -> hg38
hg38_bed = os.path.join(out_dir, 'cmpVCEPCardioBoostHg38.bed')
unmapped = hg38_bed + '.unmapped'
subprocess.run(['liftOver', '-bedPlus=9', '-tab', hg19_bed, LIFTOVER_HG19_TO_HG38,
hg38_bed, unmapped], check=True)
n_un = sum(1 for line in open(unmapped) if not line.startswith('#')) if os.path.getsize(unmapped) else 0
print(f' hg38 (liftOver hg19->hg38): {sum(1 for _ in open(hg38_bed))} variants, {n_un} unmapped')
-
# re-sort hg38 bed after liftOver
lines = [l.rstrip('\n') for l in open(hg38_bed)]
lines.sort(key=lambda l: (l.split('\t')[0], int(l.split('\t')[1])))
with open(hg38_bed, 'w') as f:
f.write('\n'.join(lines) + '\n')
hg38_bb = os.path.join(out_dir, 'cmpVCEPCardioBoostHg38.bb')
subprocess.run(['bedToBigBed', '-tab', '-type=bed9+8', '-as=' + as_path,
hg38_bed, CHROM_SIZES['hg38'], hg38_bb], check=True)
- print(f' wrote bigBeds for both assemblies')
+ print(' wrote bigBeds')
if __name__ == '__main__':
main()