Spaces:
Sleeping
Sleeping
| """ | |
| Mutation feature engineering. | |
| All features are derived from the mutation itself (from_aa, to_aa, position) | |
| and the local sequence context around the mutation site. | |
| Feature vector layout (61 dims max): | |
| [0-55] physicochemical (blosum, KD, vol, charge, burial, flags, one-hot) | |
| [56-58] secondary structure one-hot: [sst_helix, sst_strand, sst_loop] | |
| [59] relative solvent accessibility (RSA, 0=buried, 1=surface) | |
| [60] esm_masked_marginal (optional β only when model trained with ESM-2) | |
| """ | |
| import numpy as np | |
| AMINO_ACIDS = list('ACDEFGHIKLMNPQRSTVWY') | |
| AA_INDEX = {aa: i for i, aa in enumerate(AMINO_ACIDS)} | |
| # BLOSUM62 matrix | |
| BLOSUM62 = { | |
| 'A': {'A':4,'R':-1,'N':-2,'D':-2,'C':0,'Q':-1,'E':-1,'G':0,'H':-2,'I':-1,'L':-1,'K':-1,'M':-1,'F':-2,'P':-1,'S':1,'T':0,'W':-3,'Y':-2,'V':0}, | |
| 'R': {'A':-1,'R':5,'N':0,'D':-2,'C':-3,'Q':1,'E':0,'G':-2,'H':0,'I':-3,'L':-2,'K':2,'M':-1,'F':-3,'P':-2,'S':-1,'T':-1,'W':-3,'Y':-2,'V':-3}, | |
| 'N': {'A':-2,'R':0,'N':6,'D':1,'C':-3,'Q':0,'E':0,'G':0,'H':1,'I':-3,'L':-3,'K':0,'M':-2,'F':-3,'P':-2,'S':1,'T':0,'W':-4,'Y':-2,'V':-3}, | |
| 'D': {'A':-2,'R':-2,'N':1,'D':6,'C':-3,'Q':0,'E':2,'G':-1,'H':-1,'I':-3,'L':-4,'K':-1,'M':-3,'F':-3,'P':-1,'S':0,'T':-1,'W':-4,'Y':-3,'V':-3}, | |
| 'C': {'A':0,'R':-3,'N':-3,'D':-3,'C':9,'Q':-3,'E':-4,'G':-3,'H':-3,'I':-1,'L':-1,'K':-3,'M':-1,'F':-2,'P':-3,'S':-1,'T':-1,'W':-2,'Y':-2,'V':-1}, | |
| 'Q': {'A':-1,'R':1,'N':0,'D':0,'C':-3,'Q':5,'E':2,'G':-2,'H':0,'I':-3,'L':-2,'K':1,'M':0,'F':-3,'P':-1,'S':0,'T':-1,'W':-2,'Y':-1,'V':-2}, | |
| 'E': {'A':-1,'R':0,'N':0,'D':2,'C':-4,'Q':2,'E':5,'G':-2,'H':0,'I':-3,'L':-3,'K':1,'M':-2,'F':-3,'P':-1,'S':0,'T':-1,'W':-3,'Y':-2,'V':-2}, | |
| 'G': {'A':0,'R':-2,'N':0,'D':-1,'C':-3,'Q':-2,'E':-2,'G':6,'H':-2,'I':-4,'L':-4,'K':-2,'M':-3,'F':-3,'P':-2,'S':0,'T':-2,'W':-2,'Y':-3,'V':-3}, | |
| 'H': {'A':-2,'R':0,'N':1,'D':-1,'C':-3,'Q':0,'E':0,'G':-2,'H':8,'I':-3,'L':-3,'K':-1,'M':-2,'F':-1,'P':-2,'S':-1,'T':-2,'W':-2,'Y':2,'V':-3}, | |
| 'I': {'A':-1,'R':-3,'N':-3,'D':-3,'C':-1,'Q':-3,'E':-3,'G':-4,'H':-3,'I':4,'L':2,'K':-3,'M':1,'F':0,'P':-3,'S':-2,'T':-1,'W':-3,'Y':-1,'V':3}, | |
| 'L': {'A':-1,'R':-2,'N':-3,'D':-4,'C':-1,'Q':-2,'E':-3,'G':-4,'H':-3,'I':2,'L':4,'K':-2,'M':2,'F':0,'P':-3,'S':-2,'T':-1,'W':-2,'Y':-1,'V':1}, | |
| 'K': {'A':-1,'R':2,'N':0,'D':-1,'C':-3,'Q':1,'E':1,'G':-2,'H':-1,'I':-3,'L':-2,'K':5,'M':-1,'F':-3,'P':-1,'S':0,'T':-1,'W':-3,'Y':-2,'V':-2}, | |
| 'M': {'A':-1,'R':-1,'N':-2,'D':-3,'C':-1,'Q':0,'E':-2,'G':-3,'H':-2,'I':1,'L':2,'K':-1,'M':5,'F':0,'P':-2,'S':-1,'T':-1,'W':-1,'Y':-1,'V':1}, | |
| 'F': {'A':-2,'R':-3,'N':-3,'D':-3,'C':-2,'Q':-3,'E':-3,'G':-3,'H':-1,'I':0,'L':0,'K':-3,'M':0,'F':6,'P':-4,'S':-2,'T':-2,'W':1,'Y':3,'V':-1}, | |
| 'P': {'A':-1,'R':-2,'N':-2,'D':-1,'C':-3,'Q':-1,'E':-1,'G':-2,'H':-2,'I':-3,'L':-3,'K':-1,'M':-2,'F':-4,'P':7,'S':-1,'T':-1,'W':-4,'Y':-3,'V':-2}, | |
| 'S': {'A':1,'R':-1,'N':1,'D':0,'C':-1,'Q':0,'E':0,'G':0,'H':-1,'I':-2,'L':-2,'K':0,'M':-1,'F':-2,'P':-1,'S':4,'T':1,'W':-3,'Y':-2,'V':-2}, | |
| 'T': {'A':0,'R':-1,'N':0,'D':-1,'C':-1,'Q':-1,'E':-1,'G':-2,'H':-2,'I':-1,'L':-1,'K':-1,'M':-1,'F':-2,'P':-1,'S':1,'T':5,'W':-2,'Y':-2,'V':0}, | |
| 'W': {'A':-3,'R':-3,'N':-4,'D':-4,'C':-2,'Q':-2,'E':-3,'G':-2,'H':-2,'I':-3,'L':-2,'K':-3,'M':-1,'F':1,'P':-4,'S':-3,'T':-2,'W':11,'Y':2,'V':-3}, | |
| 'Y': {'A':-2,'R':-2,'N':-2,'D':-3,'C':-2,'Q':-1,'E':-2,'G':-3,'H':2,'I':-1,'L':-1,'K':-2,'M':-1,'F':3,'P':-3,'S':-2,'T':-2,'W':2,'Y':7,'V':-1}, | |
| 'V': {'A':0,'R':-3,'N':-3,'D':-3,'C':-1,'Q':-2,'E':-2,'G':-3,'H':-3,'I':3,'L':1,'K':-2,'M':1,'F':-1,'P':-2,'S':-2,'T':0,'W':-3,'Y':-1,'V':4}, | |
| } | |
| # Kyte-Doolittle hydrophobicity | |
| KD = { | |
| 'A':1.8,'R':-4.5,'N':-3.5,'D':-3.5,'C':2.5,'Q':-3.5,'E':-3.5,'G':-0.4, | |
| 'H':-3.2,'I':4.5,'L':3.8,'K':-3.9,'M':1.9,'F':2.8,'P':-1.6,'S':-0.8, | |
| 'T':-0.7,'W':-0.9,'Y':-1.3,'V':4.2, | |
| } | |
| # Residue volumes (Γ Β³) | |
| VOL = { | |
| 'A':88.6,'R':173.4,'N':114.1,'D':111.1,'C':108.5,'Q':143.8,'E':138.4,'G':60.1, | |
| 'H':153.2,'I':166.7,'L':166.7,'K':168.6,'M':162.9,'F':189.9,'P':112.7,'S':89.0, | |
| 'T':116.1,'W':227.8,'Y':193.6,'V':140.0, | |
| } | |
| # Net charge at pH 7 | |
| CHARGE = { | |
| 'A':0,'R':1,'N':0,'D':-1,'C':0,'Q':0,'E':-1,'G':0,'H':0.1,'I':0, | |
| 'L':0,'K':1,'M':0,'F':0,'P':0,'S':0,'T':0,'W':0,'Y':0,'V':0, | |
| } | |
| # Chou-Fasman helix (PΞ±) and strand (PΞ²) propensities (original 1974 values) | |
| HELIX_PROP = { | |
| 'A':1.42,'R':0.98,'N':0.67,'D':1.01,'C':0.70,'Q':1.11,'E':1.51,'G':0.57, | |
| 'H':1.00,'I':1.08,'L':1.21,'K':1.16,'M':1.45,'F':1.13,'P':0.57,'S':0.77, | |
| 'T':0.83,'W':1.08,'Y':0.69,'V':1.06, | |
| } | |
| STRAND_PROP = { | |
| 'A':0.83,'R':0.93,'N':0.89,'D':0.54,'C':1.19,'Q':1.10,'E':0.37,'G':0.75, | |
| 'H':0.87,'I':1.60,'L':1.30,'K':0.74,'M':1.05,'F':1.38,'P':0.55,'S':0.75, | |
| 'T':1.19,'W':1.37,'Y':1.47,'V':1.70, | |
| } | |
| # SST canonical names β class index (0=helix, 1=strand, 2=loop) | |
| _SST_MAP = { | |
| 'alphahelix': 0, '3-10helix': 0, | |
| 'strand': 1, 'isolatedbeta-bridge': 1, | |
| 'turn': 2, 'bend': 2, | |
| } | |
| # Condition normalisation constants | |
| TEMP_REF_K = 298.15 | |
| TEMP_SCALE = 15.0 | |
| PH_REF = 7.0 | |
| PH_SCALE = 1.5 | |
| FEATURE_NAMES = ( | |
| ['blosum62', 'delta_kd', 'delta_vol', 'delta_charge', 'burial_score', | |
| 'is_pro_to', 'is_gly_to', 'is_cys_from', 'is_pro_from', 'is_gly_from', | |
| 'abs_delta_kd', 'abs_delta_vol', 'abs_delta_charge', 'position_frac', | |
| 'temp_norm', 'ph_norm'] | |
| + [f'from_{aa}' for aa in AMINO_ACIDS] | |
| + [f'to_{aa}' for aa in AMINO_ACIDS] | |
| + ['sst_helix', 'sst_strand', 'sst_loop'] # Phase 4 β real from S1724 or Chou-Fasman proxy | |
| + ['rsa'] # Phase 4 β real from S1724 or burial-score proxy | |
| + ['esm_masked_marginal'] # Phase 3 β only when ESM-2 was available at training | |
| ) | |
| # ββ Structural feature helpers ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def burial_score(seq: str, pos: int, window: int = 4) -> float: | |
| """KD average of window neighbours β approximates hydrophobic burial.""" | |
| vals = [] | |
| for i in range(max(0, pos - window), min(len(seq), pos + window + 1)): | |
| if i != pos: | |
| vals.append(KD.get(seq[i], 0.0)) | |
| return float(np.mean(vals)) if vals else 0.0 | |
| def encode_sst(sst_str: str | None) -> list[float]: | |
| """ | |
| Map a canonical SST string (e.g. 'AlphaHelix') to one-hot [helix, strand, loop]. | |
| Unknown/None defaults to loop. | |
| """ | |
| if sst_str is None: | |
| return [0.0, 0.0, 1.0] | |
| idx = _SST_MAP.get(sst_str.lower().replace(' ', ''), 2) | |
| oh = [0.0, 0.0, 0.0] | |
| oh[idx] = 1.0 | |
| return oh | |
| def predict_sst(seq: str, pos: int, window: int = 7) -> list[float]: | |
| """ | |
| Chou-Fasman window-average secondary structure prediction. | |
| Returns one-hot [helix, strand, loop]. | |
| ~70% 3-class accuracy β used as inference-time proxy when real SST unavailable. | |
| """ | |
| start = max(0, pos - window // 2) | |
| end = min(len(seq), pos + window // 2 + 1) | |
| window_seq = seq[start:end] | |
| if not window_seq: | |
| return [0.0, 0.0, 1.0] | |
| avg_h = float(np.mean([HELIX_PROP.get(aa, 1.0) for aa in window_seq])) | |
| avg_s = float(np.mean([STRAND_PROP.get(aa, 1.0) for aa in window_seq])) | |
| if avg_h >= avg_s and avg_h > 1.0: | |
| return [1.0, 0.0, 0.0] | |
| if avg_s > avg_h and avg_s > 1.0: | |
| return [0.0, 1.0, 0.0] | |
| return [0.0, 0.0, 1.0] | |
| def approx_rsa(burial: float) -> float: | |
| """ | |
| Sequence-based RSA proxy: sigmoid of negated KD-burial score. | |
| burial > 0 (hydrophobic context) β lower RSA (more buried). | |
| Returns float in ~(0.15, 0.85). | |
| """ | |
| return float(1.0 / (1.0 + np.exp(burial * 0.8))) | |
| # ββ Main feature extractor ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract(from_aa: str, to_aa: str, position: int, | |
| sequence: str = '', conditions: dict = None, | |
| esm_score: float | None = None, | |
| rsa: float | None = None, | |
| sst: str | None = None) -> np.ndarray: | |
| """ | |
| Returns a 1-D feature vector for one single-point mutation. | |
| Phase 1-2 (always): | |
| 56 physicochemical features (BLOSUM62, KD, volume, charge, burial, flags, one-hot AA) | |
| Phase 4 (always, real or proxied): | |
| sst_helix / sst_strand / sst_loop β from S1724 CSV or Chou-Fasman proxy at inference | |
| rsa β from S1724 CSV or burial-score sigmoid proxy | |
| Phase 3 (optional): | |
| esm_masked_marginal β only when model trained with ESM-2 (None β omit, keeps 60-dim) | |
| conditions keys (pre-normalised by caller): | |
| temp_norm β (T_kelvin - 298.15) / 15.0 | |
| ph_norm β (pH - 7.0) / 1.5 | |
| """ | |
| blosum = BLOSUM62.get(from_aa, {}).get(to_aa, -4) | |
| d_kd = KD.get(to_aa, 0.0) - KD.get(from_aa, 0.0) | |
| d_vol = VOL.get(to_aa, 110.0) - VOL.get(from_aa, 110.0) | |
| d_chg = CHARGE.get(to_aa, 0.0) - CHARGE.get(from_aa, 0.0) | |
| pos_in_seq = position - 1 # 0-based | |
| burial = burial_score(sequence, pos_in_seq) if sequence else 0.0 | |
| pos_frac = position / max(len(sequence), 1) if sequence else 0.5 | |
| cond = conditions or {} | |
| temp_norm = float(cond.get('temp_norm', 0.0)) | |
| ph_norm = float(cond.get('ph_norm', 0.0)) | |
| from_oh = [1.0 if aa == from_aa else 0.0 for aa in AMINO_ACIDS] | |
| to_oh = [1.0 if aa == to_aa else 0.0 for aa in AMINO_ACIDS] | |
| # Phase 4 structural features | |
| sst_vec = encode_sst(sst) if sst is not None else predict_sst(sequence, pos_in_seq) | |
| rsa_val = float(rsa) if rsa is not None else approx_rsa(burial) | |
| vec = [ | |
| float(blosum), | |
| d_kd, | |
| d_vol, | |
| d_chg, | |
| burial, | |
| float(to_aa == 'P'), | |
| float(to_aa == 'G'), | |
| float(from_aa == 'C'), | |
| float(from_aa == 'P'), | |
| float(from_aa == 'G'), | |
| abs(d_kd), | |
| abs(d_vol), | |
| abs(d_chg), | |
| pos_frac, | |
| temp_norm, | |
| ph_norm, | |
| ] + from_oh + to_oh + sst_vec + [rsa_val] # 60 dims | |
| if esm_score is not None: | |
| vec.append(float(esm_score)) # 61st dim | |
| return np.array(vec, dtype=np.float32) | |
| def feature_matrix(mutations: list[tuple], sequence: str = '', | |
| conditions: dict = None) -> np.ndarray: | |
| """mutations: list of (from_aa, to_aa, position) tuples""" | |
| return np.vstack([extract(f, t, p, sequence, conditions) for f, t, p in mutations]) | |
| # ββ Sequence-window features for ProtStabCNN βββββββββββββββββββββββββββββββββ | |
| CNN_WINDOW = 25 # residues on each side + centre = 25 | |
| CNN_SCALARS = 6 # blosum, d_kd, d_vol, d_chg, rsa, pos_frac | |
| CNN_FEAT_DIM = CNN_WINDOW * len(AMINO_ACIDS) + CNN_SCALARS # 25Γ20 + 6 = 506 | |
| def extract_window(from_aa: str, to_aa: str, position: int, | |
| sequence: str, W: int = CNN_WINDOW) -> np.ndarray: | |
| """ | |
| 506-dim feature vector for ProtStabCNN: | |
| 500 = W=25 residue window one-hot (WT sequence, zero-padded at termini) | |
| 6 = [blosum62, d_kd, d_vol, d_chg, rsa, pos_frac] | |
| """ | |
| pos_0 = position - 1 | |
| half = W // 2 | |
| L = len(sequence) | |
| # Extract window with zero-padding at boundaries | |
| window_oh = [] | |
| for offset in range(-half, half + 1): | |
| idx = pos_0 + offset | |
| if 0 <= idx < L: | |
| aa = sequence[idx] | |
| else: | |
| aa = None # padding | |
| oh = [1.0 if (aa is not None and aa == a) else 0.0 for a in AMINO_ACIDS] | |
| window_oh.extend(oh) | |
| # 5 physicochemical scalars | |
| blosum = float(BLOSUM62.get(from_aa, {}).get(to_aa, -4)) | |
| d_kd = KD.get(to_aa, 0.0) - KD.get(from_aa, 0.0) | |
| d_vol = VOL.get(to_aa, 110.0) - VOL.get(from_aa, 110.0) | |
| d_chg = CHARGE.get(to_aa, 0.0) - CHARGE.get(from_aa, 0.0) | |
| burial = burial_score(sequence, pos_0) if sequence else 0.0 | |
| rsa = approx_rsa(burial) | |
| pos_frac = (position / max(L, 1)) if L else 0.5 | |
| return np.array(window_oh + [blosum, d_kd, d_vol, d_chg, rsa, pos_frac], | |
| dtype=np.float32) | |