| |
| """Feature 2: protein + RNA → binding probability (RnaRealismClassifierV3).""" |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import os |
| import sys |
| from pathlib import Path |
|
|
| import h5py |
| import torch |
| from transformers import AutoTokenizer |
|
|
| from common import PKG_ROOT, device, load_env, resolve |
| from encode_protein import encode_protein_to_h5 |
|
|
| sys.path.insert(0, str(PKG_ROOT / "generator")) |
| sys.path.insert(0, str(PKG_ROOT / "classifier")) |
|
|
| from labeled_dataset import make_collate |
| from model import RnaRealismClassifierV3 |
| from src.model import EsmConfig, EsmForMaskedLM |
| from src.utils import _remap_legacy_checkpoint_state_dict, base_config |
|
|
|
|
| def _load_mlm(ckpt_path: Path, tokenizer, torch_device: torch.device, model_cfg: dict) -> EsmForMaskedLM: |
| mcfg = model_cfg["model"] |
| cfg = EsmConfig(**base_config) |
| cfg.vocab_size = len(tokenizer) |
| cfg.pad_token_id = tokenizer.pad_token_id |
| cfg.mask_token_id = tokenizer.mask_token_id |
| cfg.use_FiLM = mcfg["use_FiLM"] |
| cfg.use_AdaLN = mcfg["use_AdaLN"] |
| cfg.use_gated_bias = mcfg["use_gated_bias"] |
| cfg.use_protein_conditioning_attention = mcfg["use_protein_conditioning_attention"] |
| cfg.protein_dim = mcfg["protein_dim"] |
| mlm = EsmForMaskedLM(cfg) |
| ckpt = torch.load(ckpt_path, map_location="cpu") |
| sd = _remap_legacy_checkpoint_state_dict(ckpt["model_state_dict"]) |
| mlm.load_state_dict(sd, strict=True) |
| mlm.to(torch_device) |
| return mlm |
|
|
|
|
| @torch.no_grad() |
| def score_binding( |
| protein_seq: str, |
| rna_seq: str, |
| *, |
| p_id: str = "QUERY", |
| protein_h5: Path | None = None, |
| ) -> dict: |
| load_env() |
| torch_device = torch.device(device() if torch.cuda.is_available() else "cpu") |
|
|
| if protein_h5 is None: |
| work = resolve(os.environ.get("WORKSPACE", "workspace")) |
| work.mkdir(parents=True, exist_ok=True) |
| protein_h5 = encode_protein_to_h5( |
| protein_seq, p_id=p_id, output_h5=work / f"{p_id}_vesm3b.h5" |
| ) |
|
|
| with h5py.File(protein_h5, "r") as f: |
| p_ids = [x.decode() if isinstance(x, bytes) else str(x) for x in f["p_ids"][:]] |
| if p_id not in p_ids: |
| p_id = p_ids[0] |
| idx = p_ids.index(p_id) |
| start = int(f["starts"][idx]) |
| length = int(f["lengths"][idx]) |
| emb = torch.from_numpy(f["embeddings"][start : start + length]).float() |
|
|
| rna = rna_seq.upper().replace("T", "U") |
| rna = "".join(ch for ch in rna if ch in "ACGU") |
| if not rna: |
| raise ValueError("RNA sequence is empty or invalid") |
|
|
| cls_ckpt = resolve(os.environ.get("CLASSIFIER_CKPT", "checkpoints/classifier.pt")) |
| backbone = resolve( |
| os.environ.get("BACKBONE_CKPT") |
| or os.environ.get("D3LM_BACKBONE_CKPT") |
| or "checkpoints/backbone.pt" |
| ) |
| tok_dir = resolve(os.environ.get("TOKENIZER_DIR", "generator/tokenizer")) |
|
|
| bundle = torch.load(cls_ckpt, map_location="cpu") |
| |
| model_cfg = bundle.get("model_config") or bundle["d3lm_config"] |
| train_args = bundle.get("train_args") or {} |
| data_cfg = model_cfg["data"] |
|
|
| tokenizer = AutoTokenizer.from_pretrained(str(tok_dir), trust_remote_code=True) |
| mlm = _load_mlm(backbone, tokenizer, torch_device, model_cfg) |
|
|
| model = RnaRealismClassifierV3( |
| mlm, |
| mlm.config.hidden_size, |
| int(model_cfg["model"]["protein_dim"]), |
| freeze_mode=train_args.get("freeze_mode", "conditioning_only"), |
| head_dropout=float(train_args.get("head_dropout", 0.1)), |
| cross_attn_heads=int(train_args.get("cross_attn_heads", 8)), |
| ).to(torch_device) |
|
|
| state_key = "classifier_state_dict" if "classifier_state_dict" in bundle else "model_state_dict" |
| model.load_state_dict(bundle[state_key], strict=True) |
| model.eval() |
|
|
| sample = { |
| "r_id": "query", |
| "rna": rna, |
| "p_id": p_id, |
| "protein_emb": emb, |
| "rna_len": len(rna), |
| "protein_len": int(length), |
| "label": 0, |
| } |
| append_eos = bool(data_cfg.get("append_eos_token", False)) |
| max_rna_nt = data_cfg.get("max_generated_rna_bp") |
| max_rna_nt = int(max_rna_nt) if max_rna_nt is not None else None |
| collate = make_collate(str(tok_dir), append_eos, max_rna_nt) |
| batch = collate([sample]) |
|
|
| use_bf16 = torch_device.type == "cuda" |
| with torch.autocast(device_type=torch_device.type, dtype=torch.bfloat16, enabled=use_bf16): |
| logits = model( |
| protein_cond=batch["protein_cond"].to(torch_device), |
| protein_attention_mask=batch["protein_attention_mask"].to(torch_device), |
| input_ids=batch["rna_input_ids_clean"].to(torch_device), |
| attention_mask=batch["rna_attention_mask"].to(torch_device), |
| ) |
| if isinstance(logits, (tuple, list)): |
| logits = logits[0] |
| logit_t = logits.reshape(-1)[0].float() |
| prob_t = torch.sigmoid(logit_t) |
| return { |
| "p_id": p_id, |
| "rna": rna, |
| "rna_len": len(rna), |
| "logit": float(logit_t.cpu()), |
| "binding_prob": float(prob_t.cpu()), |
| "protein_h5": str(protein_h5), |
| } |
|
|
|
|
| def main() -> None: |
| ap = argparse.ArgumentParser(description="Protein + RNA → binding probability") |
| ap.add_argument("--protein", required=True, help="AA sequence, or @path/to.fasta") |
| ap.add_argument("--rna", required=True, help="RNA sequence (ACGU/T)") |
| ap.add_argument("--p-id", default="QUERY") |
| ap.add_argument("--protein-h5", type=Path, default=None, help="Skip encoding if H5 exists") |
| args = ap.parse_args() |
|
|
| load_env() |
| protein = args.protein |
| if protein.startswith("@"): |
| text = Path(protein[1:]).read_text(encoding="utf-8") |
| lines = [ln.strip() for ln in text.splitlines() if ln.strip()] |
| if lines and lines[0].startswith(">"): |
| protein = "".join(lines[1:]) |
| if args.p_id == "QUERY": |
| args.p_id = lines[0][1:].split()[0] |
| else: |
| protein = "".join(lines) |
|
|
| out = score_binding(protein, args.rna, p_id=args.p_id, protein_h5=args.protein_h5) |
| print(json.dumps(out, ensure_ascii=False, indent=2)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|