5b4e8221e9a3ce8c7cd18d4554ee95dc4b4abfbe
lrnassar
  Thu Aug 13 17:28:29 2026 -0700
Restore BP4_Moderate strength in TP53 VCEP Curated track from Table S2. refs #37399

EvRepo's structured export emits a bare 'BP4' tag with no BP4_Moderate variant
(unlike PP3_Moderate / PM2_Supporting, which it keeps), so the VCEP Curated
track dropped the BP4 strength the panel actually applied. Restore it from the
VCEP's Table S2 bioinformatic worksheet, matched on the exact nt change:
tp53VCEPClinVar.py loads Table S2's BP4_Moderate set and upgrades a bare Met
'BP4' to 'BP4_Moderate' for the matching variant.

Validated against the ClinVar interpretation prose: where that text is parseable
its BP4 strength matches Table S2 for every missense variant. The enrichment
changes 48 variants, all exactly BP4 -> BP4_Moderate with no off-target code
changes, and removes all 34 BP4 strength disagreements in the EvRepo cross-check
(full code agreement 37% -> 43%).

diff --git src/hg/makeDb/scripts/tp53/tp53VCEPClinVar.py src/hg/makeDb/scripts/tp53/tp53VCEPClinVar.py
index 75fc4276adf..6dd7c81a4ee 100644
--- src/hg/makeDb/scripts/tp53/tp53VCEPClinVar.py
+++ src/hg/makeDb/scripts/tp53/tp53VCEPClinVar.py
@@ -18,30 +18,66 @@
 import html
 import json
 import os
 import re
 import subprocess
 import sys
 import tempfile
 import time
 import urllib.parse
 import urllib.request
 
 EVREPO_URL = ("https://erepo.genome.network/evrepo/api/classifications"
               "?gene=TP53&matchLimit=2000&format=json")
 CLINVAR_EFETCH = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
 
+# The VCEP bioinformatic worksheet (Table S2). Used to restore the strength that
+# EvRepo's structured export drops from BP4: EvRepo emits a bare "BP4" tag with
+# no BP4_Moderate variant (unlike PP3_Moderate / PM2_Supporting, which it keeps).
+# Validated against the ClinVar interpretation prose: where that text is
+# parseable, its BP4 strength matches Table S2 for every missense variant.
+SRC_S2 = "/hive/users/lrnassar/claude/RM37399/tp53_downloads/bioinformatic_worksheet.xlsx"
+_S2_BP4MOD = None
+
+
+def _norm_c(s):
+    """Strip a transcript prefix so 'NM_000546.6:c.886C>T' -> 'c.886C>T'."""
+    if not s:
+        return ''
+    return re.sub(r'^NM_000546\.[0-9]*:', '', s.strip())
+
+
+def load_s2_bp4_moderate():
+    """Return the set of c. notations that Table S2 assigns BP4_Moderate."""
+    global _S2_BP4MOD
+    if _S2_BP4MOD is not None:
+        return _S2_BP4MOD
+    out = set()
+    try:
+        import openpyxl
+        wb = openpyxl.load_workbook(SRC_S2, data_only=True)
+        ws = wb["Supplementary Table S2"]
+        for row in ws.iter_rows(min_row=4, values_only=True):
+            hgvsc = row[0]
+            code = str(row[4]).strip() if row[4] is not None else ''
+            if isinstance(hgvsc, str) and code.lower() == 'bp4_moderate':
+                out.add(_norm_c(hgvsc))
+    except Exception as ex:
+        log("  WARNING: Table S2 BP4-strength recovery unavailable: {}".format(ex))
+    _S2_BP4MOD = out
+    return out
+
 # HGVS genomic regex for hg38 (NC_000017.11) and hg19 (NC_000017.10)
 HGVS_G_RE = re.compile(r'^NC_0000(17)\.(\d+):g\.(\d+)([ACGT])>([ACGT])$')
 HGVS_G_INDEL_RE = re.compile(r'^NC_0000(17)\.(\d+):g\.(\d+)_?(\d+)?(del|ins|dup).*$')
 
 LIFTOVER_CHAINS = {
     'hg19_to_hg38': '/cluster/data/hg19/bed/liftOver/hg19ToHg38.over.chain.gz',
     'hg38_to_hg19': '/cluster/data/hg38/bed/liftOver/hg38ToHg19.over.chain.gz',
 }
 CHROM_SIZES = {
     'hg19': '/cluster/data/hg19/chrom.sizes',
     'hg38': '/cluster/data/hg38/chrom.sizes',
 }
 
 COLORS = {
     'pathogenic':             '210,0,0',
@@ -192,30 +228,35 @@
     hgvsp = ''
     if picks['hgvsp_full']:
         m = re.search(r'\(p\.[^)]+\)', picks['hgvsp_full'])
         if m:
             hgvsp = m.group(0)[1:-1]
 
     hgvsc = ''
     if picks['hgvsc']:
         hgvsc = picks['hgvsc']
     elif picks['hgvsp_full']:
         # Fallback to c. from the prose form
         m = re.search(r'c\.[^\s(]+', picks['hgvsp_full'])
         if m:
             hgvsc = 'NM_000546.6:' + m.group(0)
 
+    # Restore the BP4 strength EvRepo drops (bare "BP4" -> "BP4_Moderate") from
+    # Table S2, matched on the exact nt change.
+    if 'BP4' in met and _norm_c(hgvsc) in load_s2_bp4_moderate():
+        met = ['BP4_Moderate' if c == 'BP4' else c for c in met]
+
     return {
         'var_id': entry.get('variationId', ''),
         'caid': entry.get('caid', ''),
         'published': entry.get('publishedDate', ''),
         'classification': cls,
         'hgvsc': hgvsc,
         'hgvsp': hgvsp,
         'display': picks['display'],
         'hg38_bed': hg38,          # (start,end) or None
         'hg19_bed': hg19,
         'met_codes': met,
         'not_met_count': not_met_count,
     }