| """Deprecated MitoInteract v1 architecture retained for artifact forensics. |
| |
| The v1 checkpoint failed its evaluation gates and uses unsafe pickle |
| serialization. New work lives under ``recovery/``. Loading and inference are |
| disabled by default so this module cannot silently present an undocumented |
| legacy score as pKd. |
| """ |
|
|
| import torch |
| import torch.nn as nn |
| from transformers import EsmModel, AutoModel, AutoTokenizer |
|
|
|
|
| class MitoInteract(nn.Module): |
| def __init__( |
| self, |
| esm_model_name="facebook/esm2_t33_650M_UR50D", |
| mol_model_name="seyonec/ChemBERTa-zinc-base-v1", |
| protein_dim=1280, |
| mol_dim=768, |
| proj_dim=256, |
| n_heads=8, |
| dropout=0.1, |
| freeze_encoders=True, |
| ): |
| super().__init__() |
| self.freeze_encoders = freeze_encoders |
| self.esm = EsmModel.from_pretrained(esm_model_name) |
| self.protein_dim = protein_dim |
| self.mol_encoder = AutoModel.from_pretrained(mol_model_name) |
| self.mol_dim = mol_dim |
| if freeze_encoders: |
| for p in self.esm.parameters(): |
| p.requires_grad = False |
| for p in self.mol_encoder.parameters(): |
| p.requires_grad = False |
| self.prot_proj = nn.Sequential( |
| nn.Linear(protein_dim, proj_dim), |
| nn.LayerNorm(proj_dim), |
| nn.ReLU(), |
| nn.Dropout(dropout), |
| ) |
| self.mol_proj = nn.Sequential( |
| nn.Linear(mol_dim, proj_dim), |
| nn.LayerNorm(proj_dim), |
| nn.ReLU(), |
| nn.Dropout(dropout), |
| ) |
| self.cross_attn_mol2prot = nn.MultiheadAttention( |
| proj_dim, n_heads, dropout=dropout, batch_first=True |
| ) |
| self.cross_attn_prot2mol = nn.MultiheadAttention( |
| proj_dim, n_heads, dropout=dropout, batch_first=True |
| ) |
| self.ln_mol2prot = nn.LayerNorm(proj_dim) |
| self.ln_prot2mol = nn.LayerNorm(proj_dim) |
| fused_dim = proj_dim * 2 |
| self.mlp = nn.Sequential( |
| nn.Linear(fused_dim, 512), |
| nn.BatchNorm1d(512), |
| nn.ReLU(), |
| nn.Dropout(dropout), |
| nn.Linear(512, 256), |
| nn.BatchNorm1d(256), |
| nn.ReLU(), |
| nn.Dropout(dropout), |
| nn.Linear(256, 128), |
| nn.BatchNorm1d(128), |
| nn.ReLU(), |
| nn.Dropout(dropout), |
| nn.Linear(128, 1), |
| ) |
|
|
| def encode_protein(self, input_ids, attention_mask): |
| ctx = torch.no_grad() if self.freeze_encoders else torch.enable_grad() |
| with ctx: |
| out = self.esm(input_ids=input_ids, attention_mask=attention_mask) |
| mask = attention_mask.unsqueeze(-1).float() |
| pooled = (out.last_hidden_state * mask).sum(1) / mask.sum(1).clamp(min=1e-9) |
| return pooled, out.last_hidden_state |
|
|
| def encode_molecule(self, input_ids, attention_mask): |
| ctx = torch.no_grad() if self.freeze_encoders else torch.enable_grad() |
| with ctx: |
| out = self.mol_encoder(input_ids=input_ids, attention_mask=attention_mask) |
| return out.pooler_output, out.last_hidden_state |
|
|
| def forward( |
| self, prot_input_ids, prot_attention_mask, mol_input_ids, mol_attention_mask |
| ): |
| prot_pooled, prot_seq = self.encode_protein(prot_input_ids, prot_attention_mask) |
| mol_pooled, mol_seq = self.encode_molecule(mol_input_ids, mol_attention_mask) |
| prot_seq_proj = self.prot_proj(prot_seq) |
| mol_seq_proj = self.mol_proj(mol_seq) |
| prot_q = self.prot_proj(prot_pooled).unsqueeze(1) |
| mol_q = self.mol_proj(mol_pooled).unsqueeze(1) |
| prot_pad_mask = prot_attention_mask == 0 |
| mol_pad_mask = mol_attention_mask == 0 |
| h_prot2mol, _ = self.cross_attn_prot2mol( |
| prot_q, mol_seq_proj, mol_seq_proj, key_padding_mask=mol_pad_mask |
| ) |
| h_mol2prot, _ = self.cross_attn_mol2prot( |
| mol_q, prot_seq_proj, prot_seq_proj, key_padding_mask=prot_pad_mask |
| ) |
| h_prot2mol = self.ln_prot2mol(h_prot2mol.squeeze(1)) |
| h_mol2prot = self.ln_mol2prot(h_mol2prot.squeeze(1)) |
| fused = torch.cat([h_prot2mol, h_mol2prot], dim=-1) |
| return self.mlp(fused).squeeze(-1) |
|
|
|
|
| def load_model(checkpoint_path, device="cpu", *, allow_unsafe_legacy=False): |
| """Load the failed v1 pickle only after explicit risk acknowledgement.""" |
| if not allow_unsafe_legacy: |
| raise RuntimeError( |
| "MitoInteract v1 is deprecated and full_model.pt requires unsafe pickle " |
| "deserialization. See AUDIT.md. Pass allow_unsafe_legacy=True only for " |
| "controlled artifact forensics, never for untrusted files." |
| ) |
| checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False) |
| config = checkpoint["config"] |
| model = MitoInteract( |
| esm_model_name=config["esm_model"], |
| mol_model_name=config["mol_model"], |
| protein_dim=config["protein_dim"], |
| mol_dim=config["mol_dim"], |
| proj_dim=config["proj_dim"], |
| n_heads=config["n_heads"], |
| dropout=config["dropout"], |
| freeze_encoders=True, |
| ) |
| model.load_state_dict(checkpoint["model_state_dict"]) |
| model.eval() |
| return model, config |
|
|
|
|
| def predict_binding( |
| model, protein_seq, smiles, device="cpu", *, allow_invalid_legacy_output=False |
| ): |
| """Return the undocumented v1 score only after explicit acknowledgement.""" |
| if not allow_invalid_legacy_output: |
| raise RuntimeError( |
| "v1 output has undocumented target semantics and failed validation. " |
| "It cannot be interpreted as pKd or converted to Kd. See AUDIT.md." |
| ) |
| prot_tokenizer = AutoTokenizer.from_pretrained(model.esm.name_or_path) |
| mol_tokenizer = AutoTokenizer.from_pretrained(model.mol_encoder.name_or_path) |
|
|
| prot_enc = prot_tokenizer( |
| protein_seq, return_tensors="pt", padding=True, truncation=True, max_length=512 |
| ) |
| mol_enc = mol_tokenizer( |
| smiles, return_tensors="pt", padding=True, truncation=True, max_length=200 |
| ) |
|
|
| model = model.to(device) |
| with torch.no_grad(): |
| legacy_score = model( |
| prot_enc["input_ids"].to(device), |
| prot_enc["attention_mask"].to(device), |
| mol_enc["input_ids"].to(device), |
| mol_enc["attention_mask"].to(device), |
| ) |
|
|
| return { |
| "legacy_score": legacy_score.item(), |
| "warning": "Do not interpret this value as pKd, Kd, or validated affinity.", |
| } |
|
|