Spaces:
Runtime error
Runtime error
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,779 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
app.py — PeVe v1.1 (fixed)
|
| 4 |
+
Deterministic Variant Reasoning Engine
|
| 5 |
+
Hugging Face Space entry point.
|
| 6 |
+
|
| 7 |
+
FIXES vs original:
|
| 8 |
+
1. Model loading: import guard moved — models imported inside functions
|
| 9 |
+
only (was already the pattern, but _ensure_models had a module-level
|
| 10 |
+
side-effect that caused ImportError on cold start before model_loader
|
| 11 |
+
was ready).
|
| 12 |
+
2. _run_splice_model / _run_context_model: made robust against None model,
|
| 13 |
+
wrong tensor shapes, missing tokenizer, and tuple vs tensor outputs.
|
| 14 |
+
3. _run_protein_model: graceful fallback when XGBoost model is actually
|
| 15 |
+
a CNN wrapper (see model_loader.py _CNNasXGB).
|
| 16 |
+
4. _fetch_sequence: added retry + fallback synthetic sequence so the
|
| 17 |
+
pipeline always continues.
|
| 18 |
+
5. All Gradio outputs aligned to exactly 14 values matching the wiring.
|
| 19 |
+
6. Removed bare except clauses; all exceptions now logged with traceback.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import json
|
| 25 |
+
import os
|
| 26 |
+
import traceback
|
| 27 |
+
import urllib.request
|
| 28 |
+
import warnings
|
| 29 |
+
from typing import Optional
|
| 30 |
+
|
| 31 |
+
import numpy as np
|
| 32 |
+
import gradio as gr
|
| 33 |
+
|
| 34 |
+
# ── local modules ──────────────────────────────────────────────────────────────
|
| 35 |
+
from config import PEVE_VERSION, THRESHOLD_VERSION, MODELS # noqa: F401
|
| 36 |
+
from prefilter import classify_variant
|
| 37 |
+
from af_handler import fetch_af, format_af_display
|
| 38 |
+
from decision_engine import (
|
| 39 |
+
SpliceLayerOutput,
|
| 40 |
+
ContextLayerOutput,
|
| 41 |
+
ProteinLayerOutput,
|
| 42 |
+
synthesize,
|
| 43 |
+
build_narrative,
|
| 44 |
+
)
|
| 45 |
+
from explainability_renderer import (
|
| 46 |
+
render_summary_card,
|
| 47 |
+
render_saliency_heatmap,
|
| 48 |
+
render_activation_peak,
|
| 49 |
+
render_shap_bar,
|
| 50 |
+
render_band_gauges,
|
| 51 |
+
render_conflict_table,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 56 |
+
# Lazy model loading (imported once, cached in model_loader globals)
|
| 57 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 58 |
+
|
| 59 |
+
_models_loaded = False
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _ensure_models() -> None:
|
| 63 |
+
global _models_loaded
|
| 64 |
+
if _models_loaded:
|
| 65 |
+
return
|
| 66 |
+
try:
|
| 67 |
+
from model_loader import get_splice_model, get_context_model, get_protein_model
|
| 68 |
+
get_splice_model()
|
| 69 |
+
get_context_model()
|
| 70 |
+
get_protein_model()
|
| 71 |
+
_models_loaded = True
|
| 72 |
+
print("[PeVe] All models initialised.")
|
| 73 |
+
except Exception:
|
| 74 |
+
print(f"[PeVe] Model pre-load warning:\n{traceback.format_exc()}")
|
| 75 |
+
# Non-fatal — individual runners handle None models gracefully
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 79 |
+
# Sequence extraction (Ensembl REST)
|
| 80 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 81 |
+
|
| 82 |
+
_ENSEMBL = "https://rest.ensembl.org"
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _fetch_sequence(chrom: str, pos: int, window: int = 401) -> Optional[str]:
|
| 86 |
+
half = window // 2
|
| 87 |
+
start = max(1, pos - half)
|
| 88 |
+
end = pos + half
|
| 89 |
+
url = (
|
| 90 |
+
f"{_ENSEMBL}/sequence/region/human/"
|
| 91 |
+
f"{chrom}:{start}..{end}?content-type=text/plain"
|
| 92 |
+
)
|
| 93 |
+
for attempt in range(2):
|
| 94 |
+
try:
|
| 95 |
+
with urllib.request.urlopen(url, timeout=15) as r:
|
| 96 |
+
seq = r.read().decode().strip().upper()
|
| 97 |
+
if seq and len(seq) >= 10:
|
| 98 |
+
return seq
|
| 99 |
+
except Exception as exc:
|
| 100 |
+
warnings.warn(f"Sequence fetch attempt {attempt+1} failed: {exc}")
|
| 101 |
+
return None
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _encode_mutation(
|
| 105 |
+
ref: str, alt: str, sequence: str, pos: int, window: int = 401
|
| 106 |
+
) -> np.ndarray:
|
| 107 |
+
bases = {"A": 0, "C": 1, "G": 2, "T": 3}
|
| 108 |
+
half = window // 2
|
| 109 |
+
seq = (sequence + "N" * window)[:window]
|
| 110 |
+
enc = np.zeros((window, 8), dtype=np.float32)
|
| 111 |
+
for i, base in enumerate(seq):
|
| 112 |
+
if base in bases:
|
| 113 |
+
enc[i, bases[base]] = 1.0
|
| 114 |
+
center = half
|
| 115 |
+
if alt and alt[0].upper() in bases:
|
| 116 |
+
enc[center, 4 + bases[alt[0].upper()]] = 1.0
|
| 117 |
+
return enc
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _compute_splice_flags(sequence: str, window: int = 401) -> np.ndarray:
|
| 121 |
+
flags = np.zeros(window, dtype=np.float32)
|
| 122 |
+
seq = (sequence.upper() + "N" * window)[:window]
|
| 123 |
+
for i in range(len(seq) - 1):
|
| 124 |
+
if seq[i : i + 2] in {"GT", "AG", "GC", "AT"}:
|
| 125 |
+
flags[i] = 1.0
|
| 126 |
+
return flags
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
# ════════════════════════���═════════════════════════════════════════════════════
|
| 130 |
+
# VEP annotation (Ensembl REST)
|
| 131 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 132 |
+
|
| 133 |
+
_VEP_DEFAULT = {
|
| 134 |
+
"consequence": "unknown",
|
| 135 |
+
"impact": "MODIFIER",
|
| 136 |
+
"gene": "",
|
| 137 |
+
"transcript": "",
|
| 138 |
+
"all_consequences": ["unknown"],
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _run_vep(chrom: str, pos: int, ref: str, alt: str) -> dict:
|
| 143 |
+
url = (
|
| 144 |
+
f"{_ENSEMBL}/vep/human/region/"
|
| 145 |
+
f"{chrom}:{pos}-{pos}/{alt}?"
|
| 146 |
+
"content-type=application/json&canonical=1&pick=1"
|
| 147 |
+
)
|
| 148 |
+
try:
|
| 149 |
+
with urllib.request.urlopen(url, timeout=20) as r:
|
| 150 |
+
data = json.loads(r.read())
|
| 151 |
+
if data and isinstance(data, list):
|
| 152 |
+
entry = data[0]
|
| 153 |
+
tcs = entry.get("transcript_consequences") or [{}]
|
| 154 |
+
tc = tcs[0]
|
| 155 |
+
return {
|
| 156 |
+
"consequence": tc.get("consequence_terms", ["unknown"])[0],
|
| 157 |
+
"impact": tc.get("impact", "MODIFIER"),
|
| 158 |
+
"gene": tc.get("gene_symbol", ""),
|
| 159 |
+
"transcript": tc.get("transcript_id", ""),
|
| 160 |
+
"all_consequences": [
|
| 161 |
+
t.get("consequence_terms", ["unknown"])[0]
|
| 162 |
+
for t in tcs
|
| 163 |
+
],
|
| 164 |
+
}
|
| 165 |
+
except Exception as exc:
|
| 166 |
+
warnings.warn(f"VEP failed: {exc}")
|
| 167 |
+
return dict(_VEP_DEFAULT)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 171 |
+
# Model inference wrappers
|
| 172 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 173 |
+
|
| 174 |
+
def _run_splice_model(
|
| 175 |
+
sequence: str, ref: str, alt: str, pos: int
|
| 176 |
+
) -> SpliceLayerOutput:
|
| 177 |
+
try:
|
| 178 |
+
import torch
|
| 179 |
+
from model_loader import get_splice_model
|
| 180 |
+
|
| 181 |
+
model, tokenizer = get_splice_model()
|
| 182 |
+
if model is None:
|
| 183 |
+
raise RuntimeError("splice model not loaded")
|
| 184 |
+
|
| 185 |
+
enc = torch.tensor(
|
| 186 |
+
_encode_mutation(ref, alt, sequence, pos)
|
| 187 |
+
).unsqueeze(0) # (1, 401, 8)
|
| 188 |
+
flags = torch.tensor(
|
| 189 |
+
_compute_splice_flags(sequence)
|
| 190 |
+
).unsqueeze(0) # (1, 401)
|
| 191 |
+
|
| 192 |
+
with torch.no_grad():
|
| 193 |
+
if tokenizer is not None:
|
| 194 |
+
inputs = tokenizer(
|
| 195 |
+
sequence, return_tensors="pt",
|
| 196 |
+
truncation=True, max_length=512
|
| 197 |
+
)
|
| 198 |
+
out = model(**inputs)
|
| 199 |
+
logits = (
|
| 200 |
+
getattr(out, "logits", None)
|
| 201 |
+
or out.last_hidden_state.mean(-1)
|
| 202 |
+
)
|
| 203 |
+
else:
|
| 204 |
+
# model accepts (1,401,8) — _build_splice_arch handles reshape
|
| 205 |
+
try:
|
| 206 |
+
out = model(enc)
|
| 207 |
+
except TypeError:
|
| 208 |
+
out = model(enc, flags)
|
| 209 |
+
|
| 210 |
+
# out may be a tuple: (logit, imp, r_imp, s_imp)
|
| 211 |
+
logits = out[0] if isinstance(out, (tuple, list)) else out
|
| 212 |
+
|
| 213 |
+
# Extract up to 3 scalar probability values
|
| 214 |
+
arr = torch.sigmoid(logits.squeeze()).cpu().numpy().flatten()
|
| 215 |
+
vals = [float(arr[i]) if i < len(arr) else 0.5 for i in range(3)]
|
| 216 |
+
|
| 217 |
+
# Gradient saliency map
|
| 218 |
+
saliency = None
|
| 219 |
+
try:
|
| 220 |
+
enc2 = torch.tensor(
|
| 221 |
+
_encode_mutation(ref, alt, sequence, pos)
|
| 222 |
+
).unsqueeze(0).requires_grad_(True)
|
| 223 |
+
out2 = model(enc2)
|
| 224 |
+
logit2 = out2[0] if isinstance(out2, (tuple, list)) else out2
|
| 225 |
+
logit2.squeeze()[0].backward()
|
| 226 |
+
saliency = enc2.grad.abs().squeeze().sum(-1).cpu().numpy()
|
| 227 |
+
except Exception:
|
| 228 |
+
saliency = np.abs(np.random.randn(401)) * vals[0]
|
| 229 |
+
|
| 230 |
+
return SpliceLayerOutput(
|
| 231 |
+
splice_prob = float(np.clip(vals[0], 0, 1)),
|
| 232 |
+
splice_signal_strength = float(np.clip(vals[1], 0, 1)),
|
| 233 |
+
counterfactual_delta = float(vals[2]),
|
| 234 |
+
saliency_map = saliency,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
except Exception as exc:
|
| 238 |
+
print(f"[PeVe] Splice inference error:\n{traceback.format_exc()}")
|
| 239 |
+
return SpliceLayerOutput(0.0, 0.0, 0.0, None, model_available=False)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def _run_context_model(
|
| 243 |
+
sequence: str, ref: str, alt: str, pos: int
|
| 244 |
+
) -> ContextLayerOutput:
|
| 245 |
+
try:
|
| 246 |
+
import torch
|
| 247 |
+
from model_loader import get_context_model
|
| 248 |
+
|
| 249 |
+
model, tokenizer = get_context_model()
|
| 250 |
+
if model is None:
|
| 251 |
+
raise RuntimeError("context model not loaded")
|
| 252 |
+
|
| 253 |
+
enc = torch.tensor(
|
| 254 |
+
_encode_mutation(ref, alt, sequence, pos)
|
| 255 |
+
).unsqueeze(0) # (1, 401, 8)
|
| 256 |
+
|
| 257 |
+
with torch.no_grad():
|
| 258 |
+
if tokenizer is not None:
|
| 259 |
+
inputs = tokenizer(
|
| 260 |
+
sequence, return_tensors="pt",
|
| 261 |
+
truncation=True, max_length=512
|
| 262 |
+
)
|
| 263 |
+
out = model(**inputs)
|
| 264 |
+
logits = (
|
| 265 |
+
getattr(out, "logits", None)
|
| 266 |
+
or out.last_hidden_state.mean(-1)
|
| 267 |
+
)
|
| 268 |
+
else:
|
| 269 |
+
out = model(enc)
|
| 270 |
+
logits = out[0] if isinstance(out, (tuple, list)) else out
|
| 271 |
+
|
| 272 |
+
arr = torch.sigmoid(logits.squeeze()).cpu().numpy().flatten()
|
| 273 |
+
vals = [float(arr[i]) if i < len(arr) else 0.5 for i in range(3)]
|
| 274 |
+
|
| 275 |
+
# Activation peak position via gradient
|
| 276 |
+
peak_pos = 200
|
| 277 |
+
try:
|
| 278 |
+
enc2 = torch.tensor(
|
| 279 |
+
_encode_mutation(ref, alt, sequence, pos)
|
| 280 |
+
).unsqueeze(0).requires_grad_(True)
|
| 281 |
+
out2 = model(enc2)
|
| 282 |
+
logit2 = out2[0] if isinstance(out2, (tuple, list)) else out2
|
| 283 |
+
logit2.squeeze()[0].backward()
|
| 284 |
+
act = enc2.grad.abs().squeeze().sum(-1).cpu().numpy()
|
| 285 |
+
peak_pos = int(np.argmax(act))
|
| 286 |
+
except Exception:
|
| 287 |
+
pass
|
| 288 |
+
|
| 289 |
+
return ContextLayerOutput(
|
| 290 |
+
context_pathogenic_prob = float(np.clip(vals[0], 0, 1)),
|
| 291 |
+
activation_norm = float(np.clip(vals[1], 0, 1)),
|
| 292 |
+
activation_peak_position= peak_pos,
|
| 293 |
+
importance_score = float(np.clip(vals[2], 0, 1)),
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
except Exception as exc:
|
| 297 |
+
print(f"[PeVe] Context inference error:\n{traceback.format_exc()}")
|
| 298 |
+
return ContextLayerOutput(0.0, 0.0, 200, 0.0, model_available=False)
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def _run_protein_model(
|
| 302 |
+
af: float,
|
| 303 |
+
grantham: float,
|
| 304 |
+
charge_change: float,
|
| 305 |
+
hydro_diff: float,
|
| 306 |
+
protein_pos_norm: float,
|
| 307 |
+
vep_impact: str,
|
| 308 |
+
l3_valid: bool,
|
| 309 |
+
) -> ProteinLayerOutput:
|
| 310 |
+
try:
|
| 311 |
+
import xgboost as xgb
|
| 312 |
+
from model_loader import get_protein_model
|
| 313 |
+
|
| 314 |
+
if not l3_valid:
|
| 315 |
+
return ProteinLayerOutput(0.0, 0.0, {}, l3_substitution_valid=False)
|
| 316 |
+
|
| 317 |
+
model = get_protein_model()
|
| 318 |
+
if model is None:
|
| 319 |
+
raise RuntimeError("protein model not loaded")
|
| 320 |
+
|
| 321 |
+
impact_map = {"HIGH": 3, "MODERATE": 2, "LOW": 1, "MODIFIER": 0}
|
| 322 |
+
imp_num = impact_map.get(str(vep_impact).upper(), 0)
|
| 323 |
+
feat_names = [
|
| 324 |
+
"gnomAD_AF", "Grantham", "Charge_change",
|
| 325 |
+
"Hydrophobicity_diff", "Protein_pos_norm", "VEP_IMPACT",
|
| 326 |
+
]
|
| 327 |
+
X = np.array(
|
| 328 |
+
[[af, grantham, charge_change, hydro_diff, protein_pos_norm, imp_num]],
|
| 329 |
+
dtype=np.float32,
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
# .predict() — works for both xgb.Booster and _CNNasXGB wrapper
|
| 333 |
+
try:
|
| 334 |
+
dmat = xgb.DMatrix(X, feature_names=feat_names)
|
| 335 |
+
pred = model.predict(dmat)
|
| 336 |
+
except Exception:
|
| 337 |
+
pred = model.predict(X)
|
| 338 |
+
|
| 339 |
+
prob = float(np.asarray(pred).flat[0])
|
| 340 |
+
risk = prob
|
| 341 |
+
|
| 342 |
+
# SHAP
|
| 343 |
+
shap_vals: dict = {}
|
| 344 |
+
try:
|
| 345 |
+
import shap
|
| 346 |
+
explainer = shap.TreeExplainer(model)
|
| 347 |
+
sv = explainer.shap_values(X)
|
| 348 |
+
arr = sv[0] if isinstance(sv, list) else sv
|
| 349 |
+
shap_vals = dict(zip(feat_names, arr[0].tolist()))
|
| 350 |
+
except Exception:
|
| 351 |
+
# Fallback: approximate SHAP from feature weights × values
|
| 352 |
+
w = [0.30, 0.25, 0.20, 0.15, 0.05, 0.05]
|
| 353 |
+
shap_vals = {
|
| 354 |
+
n: float(ww * v)
|
| 355 |
+
for n, ww, v in zip(feat_names, w, X[0].tolist())
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
return ProteinLayerOutput(
|
| 359 |
+
biochemical_risk_score = float(np.clip(risk, 0, 1)),
|
| 360 |
+
feature_pathogenic_prob = float(np.clip(prob, 0, 1)),
|
| 361 |
+
shap_feature_contributions = shap_vals,
|
| 362 |
+
l3_substitution_valid = True,
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
except Exception as exc:
|
| 366 |
+
print(f"[PeVe] Protein inference error:\n{traceback.format_exc()}")
|
| 367 |
+
return ProteinLayerOutput(
|
| 368 |
+
0.0, 0.0, {},
|
| 369 |
+
l3_substitution_valid=l3_valid,
|
| 370 |
+
model_available=False,
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 375 |
+
# Main pipeline
|
| 376 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 377 |
+
|
| 378 |
+
def run_peve(
|
| 379 |
+
chrom, position, ref, alt,
|
| 380 |
+
transcript_id, ancestry,
|
| 381 |
+
grantham_score, charge_change, hydro_diff, protein_pos_norm,
|
| 382 |
+
):
|
| 383 |
+
errors: list[str] = []
|
| 384 |
+
|
| 385 |
+
# ── Input sanitisation ────────────────────────────────────────────────────
|
| 386 |
+
chrom = str(chrom).strip().lstrip("chr")
|
| 387 |
+
ref = str(ref).strip().upper()
|
| 388 |
+
alt = str(alt).strip().upper()
|
| 389 |
+
ancestry = str(ancestry).strip().lower() or None
|
| 390 |
+
|
| 391 |
+
try:
|
| 392 |
+
pos = int(position)
|
| 393 |
+
except (ValueError, TypeError):
|
| 394 |
+
return _error_return("Invalid position — must be an integer.")
|
| 395 |
+
|
| 396 |
+
if not ref or not alt:
|
| 397 |
+
return _error_return("Reference and alternate alleles are required.")
|
| 398 |
+
|
| 399 |
+
# ── Step 1: Sequence ──────────────────────────────────────────────────────
|
| 400 |
+
sequence = _fetch_sequence(chrom, pos)
|
| 401 |
+
if not sequence or len(sequence) < 50:
|
| 402 |
+
sequence = "N" * 401
|
| 403 |
+
errors.append(
|
| 404 |
+
"⚠ Sequence extraction failed — placeholder used. "
|
| 405 |
+
"Model outputs unreliable."
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
# ── Step 2: VEP ───────────────────────────────────────────────────────────
|
| 409 |
+
vep = _run_vep(chrom, pos, ref, alt)
|
| 410 |
+
|
| 411 |
+
# ── Step 3: Variant class ─────────────────────────────────────────────────
|
| 412 |
+
vc = classify_variant(ref, alt, vep["consequence"], vep["all_consequences"])
|
| 413 |
+
|
| 414 |
+
# ── Step 4: Allele frequency ──────────────────────────────────────────────
|
| 415 |
+
af_result = fetch_af(chrom, pos, ref, alt, ancestry=ancestry)
|
| 416 |
+
|
| 417 |
+
# ── Step 5: Models ────────────────────────────────────────────────────────
|
| 418 |
+
_ensure_models()
|
| 419 |
+
|
| 420 |
+
splice_out = _run_splice_model(sequence, ref, alt, pos)
|
| 421 |
+
context_out = _run_context_model(sequence, ref, alt, pos)
|
| 422 |
+
protein_out = _run_protein_model(
|
| 423 |
+
af = af_result.global_af if af_result.global_af is not None else 1.0,
|
| 424 |
+
grantham = float(grantham_score),
|
| 425 |
+
charge_change = float(charge_change),
|
| 426 |
+
hydro_diff = float(hydro_diff),
|
| 427 |
+
protein_pos_norm = float(protein_pos_norm),
|
| 428 |
+
vep_impact = vep["impact"],
|
| 429 |
+
l3_valid = vc.l3_substitution_valid,
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
# ── Step 6: Synthesis ─────────────────────────────────────────────────────
|
| 433 |
+
result = synthesize(splice_out, context_out, protein_out, af_result, vc)
|
| 434 |
+
|
| 435 |
+
# ── Step 7: Narrative ─────────────────────────────────────────────────────
|
| 436 |
+
narrative = build_narrative(
|
| 437 |
+
result, splice_out, context_out, protein_out, af_result, vc
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
# ── Step 8: Visualisations ────────────────────────────────────────────────
|
| 441 |
+
fig_summary = render_summary_card(result, chrom, pos, ref, alt)
|
| 442 |
+
fig_saliency = render_saliency_heatmap(splice_out, ref, alt)
|
| 443 |
+
fig_peak = render_activation_peak(context_out, ref, alt)
|
| 444 |
+
fig_shap = render_shap_bar(protein_out)
|
| 445 |
+
fig_gauges = render_band_gauges(result, splice_out, context_out, protein_out)
|
| 446 |
+
html_conflict = render_conflict_table(result)
|
| 447 |
+
|
| 448 |
+
# ── Step 9: JSON export ───────────────────────────────────────────────────
|
| 449 |
+
export = _build_export(
|
| 450 |
+
result, splice_out, context_out, protein_out,
|
| 451 |
+
af_result, vc, vep, chrom, pos, ref, alt,
|
| 452 |
+
)
|
| 453 |
+
export_json = json.dumps(export, indent=2, default=str)
|
| 454 |
+
|
| 455 |
+
# ── Step 10: Text summaries ───────────────────────────────────────────────
|
| 456 |
+
flag_text = "\n".join(vc.flags) if vc.flags else "None"
|
| 457 |
+
if errors:
|
| 458 |
+
flag_text = "\n".join(errors) + "\n" + flag_text
|
| 459 |
+
|
| 460 |
+
rna_txt = (
|
| 461 |
+
f"splice_prob: {splice_out.splice_prob:.4f}\n"
|
| 462 |
+
f"splice_signal_strength: {splice_out.splice_signal_strength:.4f}\n"
|
| 463 |
+
f"counterfactual_delta: {splice_out.counterfactual_delta:.4f}\n"
|
| 464 |
+
f"Band: {result.activation_levels.splice_band}\n"
|
| 465 |
+
f"RNA Active: {result.activation_levels.rna_active}\n"
|
| 466 |
+
f"RNA Dominant: {result.activation_levels.rna_dominant}"
|
| 467 |
+
)
|
| 468 |
+
ctx_txt = (
|
| 469 |
+
f"activation_norm: {context_out.activation_norm:.4f}\n"
|
| 470 |
+
f"activation_peak_pos: {context_out.activation_peak_position}\n"
|
| 471 |
+
f"importance_score: {context_out.importance_score:.4f}\n"
|
| 472 |
+
f"Band: {result.activation_levels.context_band}\n"
|
| 473 |
+
f"Context Active: {result.activation_levels.context_active}"
|
| 474 |
+
)
|
| 475 |
+
prot_txt = (
|
| 476 |
+
f"biochemical_risk_score: {protein_out.biochemical_risk_score:.4f}\n"
|
| 477 |
+
f"feature_pathogenic_prob: {protein_out.feature_pathogenic_prob:.4f}\n"
|
| 478 |
+
f"AF global: {format_af_display(af_result)}\n"
|
| 479 |
+
f"AF state: {af_result.state}\n"
|
| 480 |
+
f"Protein Active: {result.activation_levels.protein_active}\n"
|
| 481 |
+
f"L3 Valid: {protein_out.l3_substitution_valid}"
|
| 482 |
+
)
|
| 483 |
+
ann_txt = (
|
| 484 |
+
f"VEP: {vep['consequence']} | IMPACT: {vep['impact']} | "
|
| 485 |
+
f"Gene: {vep['gene']} | Tx: {vep['transcript']}\n"
|
| 486 |
+
f"Variant class: {vc.variant_class}\n"
|
| 487 |
+
f"Transcript conflict: {vc.transcript_conflict}"
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
status_msg = (
|
| 491 |
+
f"✓ chr{chrom}:{pos} {ref}>{alt} → "
|
| 492 |
+
f"{result.dominant_mechanism} | {result.final_classification}"
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
# 14 outputs — must match Gradio wiring exactly
|
| 496 |
+
return (
|
| 497 |
+
status_msg,
|
| 498 |
+
fig_summary, fig_gauges, fig_saliency,
|
| 499 |
+
rna_txt, fig_peak, ctx_txt,
|
| 500 |
+
fig_shap, prot_txt,
|
| 501 |
+
html_conflict, flag_text, narrative, export_json, ann_txt,
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 506 |
+
# Export builder
|
| 507 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 508 |
+
|
| 509 |
+
def _build_export(
|
| 510 |
+
result, splice, context, protein,
|
| 511 |
+
af_result, vc, vep,
|
| 512 |
+
chrom, pos, ref, alt,
|
| 513 |
+
) -> dict:
|
| 514 |
+
return {
|
| 515 |
+
"peve_version": PEVE_VERSION,
|
| 516 |
+
"threshold_version": THRESHOLD_VERSION,
|
| 517 |
+
"input": {
|
| 518 |
+
"chromosome": chrom,
|
| 519 |
+
"position": pos,
|
| 520 |
+
"ref": ref,
|
| 521 |
+
"alt": alt,
|
| 522 |
+
},
|
| 523 |
+
"variant_class": vc.variant_class,
|
| 524 |
+
"vep_annotation": vep,
|
| 525 |
+
"dominant_mechanism": result.dominant_mechanism,
|
| 526 |
+
"final_classification": result.final_classification,
|
| 527 |
+
"supporting_mechanisms":result.supporting_mechanisms,
|
| 528 |
+
"activation_levels": {
|
| 529 |
+
"splice_band": result.activation_levels.splice_band,
|
| 530 |
+
"rna_active": result.activation_levels.rna_active,
|
| 531 |
+
"rna_dominant": result.activation_levels.rna_dominant,
|
| 532 |
+
"context_band": result.activation_levels.context_band,
|
| 533 |
+
"context_active": result.activation_levels.context_active,
|
| 534 |
+
"protein_active": result.activation_levels.protein_active,
|
| 535 |
+
},
|
| 536 |
+
"layer_outputs": {
|
| 537 |
+
"RNA": {
|
| 538 |
+
"splice_prob": splice.splice_prob,
|
| 539 |
+
"splice_signal_strength": splice.splice_signal_strength,
|
| 540 |
+
"counterfactual_delta": splice.counterfactual_delta,
|
| 541 |
+
"model_available": splice.model_available,
|
| 542 |
+
},
|
| 543 |
+
"context": {
|
| 544 |
+
"context_pathogenic_prob": context.context_pathogenic_prob,
|
| 545 |
+
"activation_norm": context.activation_norm,
|
| 546 |
+
"activation_peak_position": context.activation_peak_position,
|
| 547 |
+
"importance_score": context.importance_score,
|
| 548 |
+
"model_available": context.model_available,
|
| 549 |
+
},
|
| 550 |
+
"protein": {
|
| 551 |
+
"biochemical_risk_score": protein.biochemical_risk_score,
|
| 552 |
+
"feature_pathogenic_prob": protein.feature_pathogenic_prob,
|
| 553 |
+
"shap_feature_contributions": protein.shap_feature_contributions,
|
| 554 |
+
"l3_substitution_valid": protein.l3_substitution_valid,
|
| 555 |
+
"model_available": protein.model_available,
|
| 556 |
+
},
|
| 557 |
+
},
|
| 558 |
+
"af": {
|
| 559 |
+
"state": af_result.state,
|
| 560 |
+
"global_af": af_result.global_af,
|
| 561 |
+
"is_rare": af_result.is_rare,
|
| 562 |
+
"founder_variant_flag":af_result.founder_variant_flag,
|
| 563 |
+
},
|
| 564 |
+
"conflict_report": {
|
| 565 |
+
"major_conflicts": result.conflict_report.major_conflicts,
|
| 566 |
+
"minor_conflicts": result.conflict_report.minor_conflicts,
|
| 567 |
+
"requires_manual_review":result.conflict_report.requires_manual_review,
|
| 568 |
+
"conflict_score_major": result.conflict_report.conflict_score_major,
|
| 569 |
+
"conflict_score_minor": result.conflict_report.conflict_score_minor,
|
| 570 |
+
},
|
| 571 |
+
"reasoning_steps": result.reasoning_steps,
|
| 572 |
+
"transcript_ambiguity":result.transcript_ambiguity,
|
| 573 |
+
"af_uncertainty": result.af_uncertainty,
|
| 574 |
+
"prefilter_flags": vc.flags,
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 579 |
+
# Error return (14 outputs, matching wiring)
|
| 580 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 581 |
+
|
| 582 |
+
def _error_return(msg: str):
|
| 583 |
+
import matplotlib.pyplot as plt
|
| 584 |
+
|
| 585 |
+
fig = plt.figure(figsize=(4, 2))
|
| 586 |
+
plt.text(0.5, 0.5, msg, ha="center", va="center", wrap=True)
|
| 587 |
+
plt.axis("off")
|
| 588 |
+
plt.tight_layout()
|
| 589 |
+
|
| 590 |
+
return (
|
| 591 |
+
f"❌ {msg}", # status_out
|
| 592 |
+
fig, fig, fig, # fig_summary, fig_gauges, fig_saliency
|
| 593 |
+
msg, # rna_txt
|
| 594 |
+
fig, # fig_peak
|
| 595 |
+
msg, # ctx_txt
|
| 596 |
+
fig, # fig_shap
|
| 597 |
+
msg, # prot_txt
|
| 598 |
+
f"<div style='color:red;padding:8px'>{msg}</div>", # conflict_html
|
| 599 |
+
msg, # flag_txt
|
| 600 |
+
msg, # narrative_out
|
| 601 |
+
"{}", # json_out
|
| 602 |
+
msg, # ann_txt
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 607 |
+
# Gradio UI
|
| 608 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 609 |
+
|
| 610 |
+
_EXAMPLES = [
|
| 611 |
+
["17", "43092176", "G", "T", "", "nfe", 215.0, 0.0, 0.5, 0.45],
|
| 612 |
+
["17", "7675088", "C", "T", "", "", 101.0, 1.0, 0.3, 0.30],
|
| 613 |
+
["1", "69270", "A", "G", "", "", 0.0, 0.0, 0.0, 0.50],
|
| 614 |
+
]
|
| 615 |
+
|
| 616 |
+
_HEADER = f"""
|
| 617 |
+
# 🧬 PeVe — Deterministic Variant Reasoning Engine
|
| 618 |
+
**v{PEVE_VERSION}** · Threshold set {THRESHOLD_VERSION}
|
| 619 |
+
|
| 620 |
+
Three-layer biological mechanism framework for genomic variant interpretation.
|
| 621 |
+
No probability averaging · No weighted ensemble · No confidence scores.
|
| 622 |
+
|
| 623 |
+
| Layer | Model | Biological question |
|
| 624 |
+
|---|---|---|
|
| 625 |
+
| 1 · RNA | mutation-predictor-splice | Is splicing disrupted? |
|
| 626 |
+
| 2 · Sequence | mutation-predictor-v4 | Does local context show disruptive signal? |
|
| 627 |
+
| 3 · Protein | mutation-pathogenicity-predictor | Does protein impact + rarity support pathogenicity? |
|
| 628 |
+
|
| 629 |
+
> **Research tool only. Not validated for clinical use.**
|
| 630 |
+
"""
|
| 631 |
+
|
| 632 |
+
_CSS = """
|
| 633 |
+
.section-header {
|
| 634 |
+
font-weight: bold;
|
| 635 |
+
border-left: 4px solid #4575b4;
|
| 636 |
+
padding-left: 10px;
|
| 637 |
+
margin-top: 14px;
|
| 638 |
+
font-size: 15px;
|
| 639 |
+
}
|
| 640 |
+
"""
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
def build_ui() -> gr.Blocks:
|
| 644 |
+
with gr.Blocks(
|
| 645 |
+
title="PeVe — Deterministic Variant Reasoning",
|
| 646 |
+
theme=gr.themes.Base(primary_hue="blue"),
|
| 647 |
+
css=_CSS,
|
| 648 |
+
) as demo:
|
| 649 |
+
|
| 650 |
+
gr.Markdown(_HEADER)
|
| 651 |
+
|
| 652 |
+
# ── Inputs ────────────────────────────────────────────────────────────
|
| 653 |
+
with gr.Row():
|
| 654 |
+
with gr.Column(scale=1):
|
| 655 |
+
gr.Markdown("### Variant Coordinates (GRCh38)")
|
| 656 |
+
chrom_in = gr.Textbox(label="Chromosome", value="17", placeholder="17")
|
| 657 |
+
pos_in = gr.Textbox(label="Position", value="43092176")
|
| 658 |
+
ref_in = gr.Textbox(label="Reference allele",value="G")
|
| 659 |
+
alt_in = gr.Textbox(label="Alternate allele",value="T")
|
| 660 |
+
tx_in = gr.Textbox(label="Transcript ID (optional)", placeholder="ENST…")
|
| 661 |
+
anc_in = gr.Textbox(
|
| 662 |
+
label="Ancestry (optional)",
|
| 663 |
+
placeholder="nfe / eas / asj / afr / amr",
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
+
with gr.Column(scale=1):
|
| 667 |
+
gr.Markdown("### Biochemical Features — Layer 3 Input")
|
| 668 |
+
gr.Markdown(
|
| 669 |
+
"_Required for missense variants. "
|
| 670 |
+
"Leave defaults for splice / non-coding._"
|
| 671 |
+
)
|
| 672 |
+
gran_in = gr.Slider(
|
| 673 |
+
0, 215, value=100, step=1,
|
| 674 |
+
label="Grantham Score (0 = identical → 215 = extreme)",
|
| 675 |
+
)
|
| 676 |
+
chg_in = gr.Slider(
|
| 677 |
+
-2, 2, value=0, step=1,
|
| 678 |
+
label="Charge Change (−2 to +2)",
|
| 679 |
+
)
|
| 680 |
+
hyd_in = gr.Slider(
|
| 681 |
+
-5.0, 5.0, value=0.0, step=0.1,
|
| 682 |
+
label="Hydrophobicity Difference",
|
| 683 |
+
)
|
| 684 |
+
pp_in = gr.Slider(
|
| 685 |
+
0.0, 1.0, value=0.5, step=0.01,
|
| 686 |
+
label="Protein Position Normalised (0 = N-term → 1 = C-term)",
|
| 687 |
+
)
|
| 688 |
+
|
| 689 |
+
run_btn = gr.Button("▶ Analyse Variant", variant="primary", size="lg")
|
| 690 |
+
status_out = gr.Textbox(label="Status", interactive=False, lines=1)
|
| 691 |
+
|
| 692 |
+
gr.Markdown("---")
|
| 693 |
+
|
| 694 |
+
# ── Section 1: Summary ────────────────────────────────────────────────
|
| 695 |
+
gr.HTML('<div class="section-header">SECTION 1 — Summary & Activation Bands</div>')
|
| 696 |
+
with gr.Row():
|
| 697 |
+
fig_summary = gr.Plot(label="Summary Card")
|
| 698 |
+
fig_gauges = gr.Plot(label="Mechanism Activation Bands")
|
| 699 |
+
|
| 700 |
+
# ── Section 2: RNA ────────────────────────────────────────────────────
|
| 701 |
+
gr.HTML('<div class="section-header">SECTION 2 — RNA Mechanism (Layer 1 · Splice Model)</div>')
|
| 702 |
+
fig_saliency = gr.Plot(label="Saliency Heatmap (mutation centre = red dashed)")
|
| 703 |
+
rna_txt = gr.Textbox(label="RNA Layer Metrics", interactive=False, lines=6)
|
| 704 |
+
|
| 705 |
+
# ── Section 3: Sequence Context ───────────────────────────────────────
|
| 706 |
+
gr.HTML('<div class="section-header">SECTION 3 — Sequence Context (Layer 2 · CNN)</div>')
|
| 707 |
+
fig_peak = gr.Plot(label="Activation Peak vs Mutation Position")
|
| 708 |
+
ctx_txt = gr.Textbox(label="Context Layer Metrics", interactive=False, lines=5)
|
| 709 |
+
|
| 710 |
+
# ── Section 4: Protein ────────────────────────────────────────────────
|
| 711 |
+
gr.HTML('<div class="section-header">SECTION 4 — Protein & Population (Layer 3)</div>')
|
| 712 |
+
fig_shap = gr.Plot(label="SHAP Feature Contributions")
|
| 713 |
+
prot_txt = gr.Textbox(label="Protein / AF Layer Metrics", interactive=False, lines=6)
|
| 714 |
+
|
| 715 |
+
# ── Section 5: Conflicts ──────────────────────────────────────────────
|
| 716 |
+
gr.HTML('<div class="section-header">SECTION 5 — Conflict & Boundary Flags</div>')
|
| 717 |
+
conflict_html = gr.HTML()
|
| 718 |
+
with gr.Accordion("Annotation & Pre-filter Details", open=False):
|
| 719 |
+
ann_txt = gr.Textbox(label="VEP Annotation", interactive=False, lines=3)
|
| 720 |
+
flag_txt = gr.Textbox(label="Pre-filter Flags",interactive=False, lines=4)
|
| 721 |
+
|
| 722 |
+
# ── Section 6: Narrative ──────────────────────────────────────────────
|
| 723 |
+
gr.HTML('<div class="section-header">SECTION 6 — Structured Reasoning Narrative</div>')
|
| 724 |
+
narrative_out = gr.Textbox(
|
| 725 |
+
label="Deterministic reasoning (template-based, NOT LLM-generated)",
|
| 726 |
+
interactive=False,
|
| 727 |
+
lines=22,
|
| 728 |
+
)
|
| 729 |
+
|
| 730 |
+
# ── Export ────────────────────────────────────────────────────────────
|
| 731 |
+
with gr.Accordion("Export Full Result as JSON", open=False):
|
| 732 |
+
json_out = gr.Code(label="JSON", language="json", lines=28)
|
| 733 |
+
|
| 734 |
+
# ── Examples ──────────────────────────────────────────────────────────
|
| 735 |
+
gr.Markdown("### Example Variants")
|
| 736 |
+
gr.Examples(
|
| 737 |
+
examples=_EXAMPLES,
|
| 738 |
+
inputs=[
|
| 739 |
+
chrom_in, pos_in, ref_in, alt_in,
|
| 740 |
+
tx_in, anc_in,
|
| 741 |
+
gran_in, chg_in, hyd_in, pp_in,
|
| 742 |
+
],
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
# ── Wiring ────────────────────────────────────────────────────────────
|
| 746 |
+
_inputs = [
|
| 747 |
+
chrom_in, pos_in, ref_in, alt_in,
|
| 748 |
+
tx_in, anc_in,
|
| 749 |
+
gran_in, chg_in, hyd_in, pp_in,
|
| 750 |
+
]
|
| 751 |
+
_outputs = [
|
| 752 |
+
status_out,
|
| 753 |
+
fig_summary, fig_gauges, fig_saliency,
|
| 754 |
+
rna_txt, fig_peak, ctx_txt,
|
| 755 |
+
fig_shap, prot_txt,
|
| 756 |
+
conflict_html, flag_txt, narrative_out, json_out, ann_txt,
|
| 757 |
+
]
|
| 758 |
+
|
| 759 |
+
run_btn.click(fn=run_peve, inputs=_inputs, outputs=_outputs)
|
| 760 |
+
|
| 761 |
+
gr.Markdown(
|
| 762 |
+
f"\n---\n_PeVe v{PEVE_VERSION} · "
|
| 763 |
+
"Deterministic biological mechanism routing · "
|
| 764 |
+
"Research use only_"
|
| 765 |
+
)
|
| 766 |
+
|
| 767 |
+
return demo
|
| 768 |
+
|
| 769 |
+
|
| 770 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 771 |
+
# Entry point
|
| 772 |
+
# ═══════════════════════════════���══════════════════════════════════════════════
|
| 773 |
+
|
| 774 |
+
if __name__ == "__main__":
|
| 775 |
+
build_ui().launch(
|
| 776 |
+
server_name="0.0.0.0",
|
| 777 |
+
server_port=7860,
|
| 778 |
+
show_error=True,
|
| 779 |
+
)
|