""" protein_feat.py — Protein feature processing for BioInteract Space demo. Adapted from the training version: removed dependency on internal src.utils.paths module so this file runs as a standalone module. ESM-2 embeddings are computed on-the-fly via transformers in app.py. """ import os import json import torch import numpy as np from typing import Optional # ============================================================ # Standard amino acid vocabulary # ============================================================ AMINO_ACIDS = list('ACDEFGHIKLMNPQRSTVWY') AA_TO_IDX = {aa: i for i, aa in enumerate(AMINO_ACIDS)} UNK_AA_IDX = len(AMINO_ACIDS) # ============================================================ # Residue-level physicochemical descriptors # ============================================================ HYDROPHOBICITY = { 'A': 1.8, 'R': -4.5, 'N': -3.5, 'D': -3.5, 'C': 2.5, 'E': -3.5, 'Q': -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, } MOL_WEIGHT = { 'A': 89, 'R': 174, 'N': 132, 'D': 133, 'C': 121, 'E': 147, 'Q': 146, 'G': 75, 'H': 155, 'I': 131, 'L': 131, 'K': 146, 'M': 149, 'F': 165, 'P': 115, 'S': 105, 'T': 119, 'W': 204, 'Y': 181, 'V': 117, } CHARGE = { 'A': 0, 'R': 1, 'N': 0, 'D': -1, 'C': 0, 'E': -1, 'Q': 0, '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, } POLARITY = { 'A': 0, 'R': 1, 'N': 1, 'D': 1, 'C': 0, 'E': 1, 'Q': 1, 'G': 0, 'H': 1, 'I': 0, 'L': 0, 'K': 1, 'M': 0, 'F': 0, 'P': 0, 'S': 1, 'T': 1, 'W': 0, 'Y': 1, 'V': 0, } def residue_physicochemical_features(sequence: str) -> torch.Tensor: """ Compute per-residue physicochemical feature vectors. Returns tensor of shape (L, 4). """ features = [] for aa in sequence: h = HYDROPHOBICITY.get(aa, 0.0) / 4.5 w = MOL_WEIGHT.get(aa, 130.0) / 204.0 c = CHARGE.get(aa, 0.0) p = POLARITY.get(aa, 0.0) features.append([h, w, c, p]) return torch.tensor(features, dtype=torch.float) # ============================================================ # Domain annotation helpers # ============================================================ DOMAIN_TYPES = [ 'Pkinase', 'Pkinase_Tyr', '7tm_1', '7tm_2', '7tm_3', 'Trypsin', 'Peptidase_C1', 'Metallopep', 'zf-CCHH', 'SH2', 'SH3_1', 'PH', 'RRM_1', 'Ion_trans', 'Ank', 'WD40', 'LRR_1', 'EGF', 'Ig', 'ABC_tran', 'Hormone_recep', 'HATPase_c', 'Helicase_C', 'RVT_1', 'Topoisom_bac', 'NONE', ] DOMAIN_TO_IDX = {d: i for i, d in enumerate(DOMAIN_TYPES)} def residue_domain_labels( sequence_length: int, domain_info: Optional[dict] = None ) -> torch.LongTensor: """ Assign each residue a domain type index. Returns LongTensor of shape (L,). Residues without annotation get the 'NONE' label. """ labels = torch.full( (sequence_length,), DOMAIN_TO_IDX['NONE'], dtype=torch.long ) if domain_info is not None and 'domains' in domain_info: for dom in domain_info['domains']: dom_type = dom.get('type', 'NONE') if dom_type not in DOMAIN_TO_IDX: dom_type = 'NONE' idx = DOMAIN_TO_IDX[dom_type] start = dom.get('start', 0) end = min(dom.get('end', sequence_length), sequence_length) labels[start:end] = idx return labels