Spaces:
Running
Running
| """Per-prediction model certainty — the glass-box "how sure is ESM-2?" signal. | |
| Every ΔLL score answers "is this substitution favorable?" — but not "how much | |
| should I trust that call?". This module adds the second axis, honestly: it | |
| measures how SHARP ESM-2's amino-acid distribution is at each position. A | |
| well-conserved position where the model overwhelmingly prefers one residue | |
| (low entropy) is one it's confident about; a flexible position where many | |
| residues are near-equiprobable (high entropy) is one where any single call is | |
| a weaker signal — and we say so rather than hide it. | |
| Crucially this is NOT calibrated against wet-lab outcomes (we don't claim it | |
| is): it's the model's own posterior confidence, surfaced instead of buried. | |
| The bench-calibrated axis is the separate active-learning / calibration work. | |
| Cost is zero extra compute: softmax is shift-invariant, so the full | |
| per-position posterior over the 20 amino acids is recoverable from the ΔLL | |
| table we already computed — ``p(a) ∝ exp(ΔLL_a)`` with ``ΔLL_wt = 0`` — no | |
| extra ESM-2 forward pass. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import re | |
| from typing import Dict, Optional, Sequence, Tuple | |
| import numpy as np | |
| # 20 canonical AAs; H_max is the entropy of a uniform posterior over them. | |
| _N_AA = 20 | |
| _H_MAX = math.log(_N_AA) | |
| _LABEL_RE = re.compile(r"^([A-Za-z])(\d+)([A-Za-z*])$") # "F76Y" | |
| def _entropy_confidence(delta_lls: Sequence[float]) -> float: | |
| """Confidence in [0,1] for one position from its 19 mutant ΔLLs. | |
| Reconstruct the posterior p(a) ∝ exp(ΔLL_a) (wt's ΔLL is 0 by definition), | |
| then confidence = 1 - H(p)/H_max. 1.0 = the model is certain (one residue | |
| dominates); 0.0 = maximally uncertain (flat over all 20).""" | |
| vals = np.asarray(list(delta_lls) + [0.0], dtype=np.float64) # + implicit WT | |
| vals = vals - vals.max() # numerically-stable softmax | |
| p = np.exp(vals) | |
| p = p / p.sum() | |
| nz = p[p > 0] | |
| h = float(-(nz * np.log(nz)).sum()) | |
| conf = 1.0 - h / _H_MAX | |
| return float(min(1.0, max(0.0, conf))) | |
| def position_confidence_from_scores(scores_df) -> Dict[int, float]: | |
| """position (0-indexed) -> confidence in [0,1], from the long-format ΔLL | |
| DataFrame (columns: position, delta_ll, ...). Positions with fewer than a | |
| near-complete substitution set are still scored on whatever is present.""" | |
| out: Dict[int, float] = {} | |
| if scores_df is None or len(scores_df) == 0: | |
| return out | |
| for pos, group in scores_df.groupby("position"): | |
| out[int(pos)] = _entropy_confidence([float(x) for x in group["delta_ll"].values]) | |
| return out | |
| def _parse_positions(mutations: str) -> list: | |
| """'V60L,F76Y' -> [('V60L', 59), ('F76Y', 75)] (label, 0-indexed pos).""" | |
| out = [] | |
| for tok in re.split(r"[,\s;]+", (mutations or "").strip()): | |
| m = _LABEL_RE.match(tok.strip()) | |
| if not m: | |
| continue | |
| pos = int(m.group(2)) - 1 | |
| if pos >= 0: | |
| out.append((tok.strip(), pos)) | |
| return out | |
| def variant_confidence( | |
| mutations: str, pos_conf: Dict[int, float], | |
| ) -> Tuple[Optional[float], str]: | |
| """Overall confidence for a variant = the WEAKEST-LINK of its mutated | |
| positions (one uncertain position honestly undermines the whole call). | |
| Returns (confidence or None if no scorable position, weakest-site label).""" | |
| sites = _parse_positions(mutations) | |
| scored = [(lbl, pos_conf[pos]) for (lbl, pos) in sites if pos in pos_conf] | |
| if not scored: | |
| return (None, "") | |
| weakest_label, weakest = min(scored, key=lambda t: t[1]) | |
| return (round(weakest, 4), weakest_label) | |
| def attach_confidence(rows: list, pos_conf: Dict[int, float]) -> list: | |
| """In-place: add ``Confidence`` (0-1, weakest-link) and | |
| ``Confidence_Weakest`` (the label of the least-certain changed residue) to | |
| each variant row dict that has a ``Mutations_AA``. WT / mutation-less rows | |
| get Confidence = None. Returns the same list for chaining.""" | |
| for r in rows: | |
| if not isinstance(r, dict): | |
| continue | |
| muts = str(r.get("Mutations_AA", "") or "") | |
| if not muts: | |
| r["Confidence"] = None | |
| r["Confidence_Weakest"] = "" | |
| continue | |
| conf, weakest = variant_confidence(muts, pos_conf) | |
| r["Confidence"] = conf | |
| r["Confidence_Weakest"] = weakest | |
| return rows | |