6796b337521a06767307b5c273bf89903d5a3bd6
lrnassar
Tue Aug 11 17:05:08 2026 -0700
Fix Variant Evidence Summary SpliceAI/BP7 issues per code review. refs #37446
- Use the released spliceAIsnvsMasked.bb instead of a superseded symlink; the
older /gbdb/hg38/bbi/spliceAi.bb pointed at a stale file the live track no
longer serves.
- Distinguish a SpliceAI lookup miss from a measured 0.00 in the mouseover. A
miss now reads "no record (below the 0.02 reporting floor)"; BP7 still applies
because the file's 0.02 floor means a missing score is below 0.1.
- Correct the BP7 mouseover threshold text from "<= 0.1" to "< 0.1" to match the
code, docstring, and makedoc.
- Record PM1 suppression in the codeNotes field so it matches the autoSql
description instead of leaving the field empty on every row.
diff --git src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPProvisionalClass.py src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPProvisionalClass.py
index 0007e034422..4d991c786b6 100644
--- src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPProvisionalClass.py
+++ src/hg/makeDb/scripts/cardiomyopathyVCEP/cmpVCEPProvisionalClass.py
@@ -33,31 +33,31 @@
import argparse, json, os, re, subprocess, sys
from collections import defaultdict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cmpVCEPClinDomains import parse_mane_record
OUR_GENES = ['MYH7', 'MYBPC3', 'TNNT2', 'TNNI3', 'TPM1', 'ACTC1', 'MYL2', 'MYL3']
WORKDIR = '/hive/users/lrnassar/claude/RM37446'
B3_BED = f'{WORKDIR}/cmpVCEPAFfrequencies/cmpVCEPAFfrequenciesHg38.bed'
B4_BED = f'{WORKDIR}/cmpVCEPRevel/cmpVCEPRevelHg38.bed'
B1_BED = f'{WORKDIR}/cmpVCEPClinDomains/cmpVCEPClinDomainsHg38.bed'
EVREPO_JSON = f'{WORKDIR}/cmp_downloads/erepo/cardiomyopathyVCEP_classifications.json'
ANNOT_TSV = f'{WORKDIR}/cmpVCEPAnnotate/cmpVCEPAnnotations.hg38.tsv'
-SPLICEAI_BB = '/gbdb/hg38/bbi/spliceAi.bb'
+SPLICEAI_BB = '/gbdb/hg38/bbi/spliceAIsnvsMasked.bb' # released masked-SNV file (what the live UCSC track serves)
# Per-gene thresholds (from CSpec - NOT invented here)
BS1_THRESHOLDS = {'MYBPC3': 0.0002}
DEFAULT_BS1 = 0.0001
BA1_THRESHOLD = 0.001
PM2_SUPPORTING_THRESHOLD = 0.00004
SPLICE_SAFETY_THRESHOLD = 0.20 # SpliceAI delta flagged as possible splice impact (informational)
# BP7: synonymous with no predicted splice impact. The CM VCEP set this at SpliceAI < 0.1 and
# removed the conservation requirement, per Walker 2023 (PMID 37352859).
BP7_SPLICE_MAX = 0.10
# NC_ accession (hg38) -> chrom, for parsing EvRepo genomic HGVS (leave-one-out keys)
NC_HG38 = {
'NC_000001.11': 'chr1', 'NC_000003.12': 'chr3', 'NC_000011.10': 'chr11',
'NC_000012.12': 'chr12', 'NC_000014.9': 'chr14', 'NC_000015.10': 'chr15',
@@ -426,31 +426,36 @@
if gene != 'MYBPC3' and (so & TRUNCATING_SO):
cdna, J = a.get('cdnaPos'), nmd_junction.get(gene)
nmd_escape = (a.get('exonNum') is not None and a.get('exonNum') == a.get('exonTotal')) \
or (cdna is not None and J is not None and cdna > J - 50)
if nmd_escape:
codes.add('PM4_Supporting')
last = a.get('exonNum') == a.get('exonTotal')
where = 'last exon' if last else 'within 50 nt of the last exon-exon junction'
pm4_ev = f'truncating, escapes NMD ({where})'
elif gene != 'MYBPC3' and 'stop_lost' in so:
codes.add('PM4_Supporting')
pm4_ev = 'stop-loss variant'
# BP7 - synonymous with no predicted splice impact (SpliceAI < 0.1, per Walker 2023
# PMID 37352859). The CM VCEP removed the conservation requirement, so no phyloP gate.
- sa_score = spliceai.get((chrom, pos1, ref, alt), 0.0)
+ # The SpliceAI file has a 0.02 reporting floor, so a lookup miss means the true
+ # score is below 0.02 (hence below 0.1) - BP7 still applies. Track presence so the
+ # mouseover does not print a missing record as a measured "0.00".
+ sa_hit = spliceai.get((chrom, pos1, ref, alt))
+ sa_present = sa_hit is not None
+ sa_score = sa_hit if sa_present else 0.0
if is_synonymous and not args.no_spliceai:
if sa_score < BP7_SPLICE_MAX:
codes.add('BP7_Supporting')
# CSpec exclusion: PM1 must NOT be combined with PM5. GN002 PM5: "use of PM5 is most
# appropriate since it is variant specific" -> keep PM5, drop PM1.
pm1_suppressed = False
if 'PM1_Moderate' in codes and 'PM5_Moderate' in codes:
codes.discard('PM1_Moderate')
pm1_suppressed = True
# CSpec is silent on PM1+PS1; flag as a possible double-count (do not suppress).
if 'PM1_Moderate' in codes and 'PS1_Strong' in codes:
notes.append('PM1 and PS1 co-occur here, a possible double-count (open VCEP question)')
# Splice signal (informational): SpliceAI >= 0.20 flags possible splice impact.
@@ -477,57 +482,65 @@
if ps1_codon:
ev.append(f'Known variants at residue {ps1_codon} (EvRepo): the same amino-acid change is '
f'VCEP P/LP → supports PS1_Strong')
if pm5_codon:
ev.append(f'Known variants at residue {pm5_codon} (EvRepo): a different VCEP P/LP change '
f'exists at this residue → supports PM5_Moderate')
if pm1_hit:
if pm1_suppressed:
ev.append(f'PM1 hotspot region: within the {gene} hotspot (HCM only); not counted here, '
f'PM5 (variant-specific) is preferred per CSpec')
else:
ev.append(f'PM1 hotspot region: within the {gene} hotspot (HCM only) → supports PM1_Moderate')
if pm4_ev:
ev.append(f'Protein-truncating: {pm4_ev} → supports PM4_Supporting')
if 'BP7_Supporting' in codes:
- ev.append(f'Synonymous, splicing (SpliceAI): {sa_score:.2f}, no predicted impact '
- f'(≤ 0.1, per Walker 2023) → supports BP7_Supporting')
+ sa_txt = f'{sa_score:.2f}' if sa_present else 'no record (below the 0.02 reporting floor)'
+ ev.append(f'Synonymous, splicing (SpliceAI): {sa_txt}, no predicted impact '
+ f'(< 0.1, per Walker 2023) → supports BP7_Supporting')
if splice_safety == 'yes':
ev.append(f'Splicing (SpliceAI): {sa_score:.2f}, possible splice impact (informational)')
mo = ['Variant evidence (not a VCEP classification)
',
f'{gene} · {chrom}:{pos1} {ref}>{alt}']
if a.get('hgvsp'):
mo.append(f' · {a["hgvsp"]}')
mo.append(f' · {kind}
')
if ev:
for line in ev:
mo.append(line + '
')
else:
mo.append('No computable evidence at this position.
')
if notes:
mo.append('' + ' | '.join(notes) + '
')
mo.append('
Shows the computable evidence and the ACMG criteria it would support. '
'Not a VCEP review or classification; clinical/functional evidence '
'(PS2/PS3/PS4/PP1/PP4/BS3/BS4) is not included.')
mouseover = ''.join(mo)
+ # codeNotes data field: contested/double-count notes plus any suppressed code.
+ # The mouseover renders suppression inline as an evidence line, so it is kept out
+ # of the red-span `notes` above to avoid duplication; it belongs in the data field.
+ code_notes = list(notes)
+ if pm1_suppressed:
+ code_notes.append('PM1 present but not combined with PM5 (PM5 preferred per CSpec)')
+
name = f'{gene}_{pos1}_{ref}>{alt}'
bed_lines.append('\t'.join([
chrom, str(v['start']), str(v['end']), name, '0', v['strand'],
str(v['start']), str(v['end']), color, gene, ref, alt, kind,
- applied_str, disease_tag, ' | '.join(notes), splice_safety, mouseover,
+ applied_str, disease_tag, ' | '.join(code_notes), splice_safety, mouseover,
]))
print(f' features: {n_features}')
print(f' code firing counts: {dict(sorted(code_counts.items()))}')
bed_lines.sort(key=lambda l: (l.split('\t')[0], int(l.split('\t')[1])))
as_path = os.path.join(out_dir, 'cmpVCEPProvisionalClass.as')
with open(as_path, 'w') as f:
f.write(AUTOSQL)
hg38_bed = os.path.join(out_dir, 'cmpVCEPProvisionalClassHg38.bed')
with open(hg38_bed, 'w') as f:
f.write('\n'.join(bed_lines) + '\n')
print(f' wrote {len(bed_lines)} BED features -> {hg38_bed}')