ProRiboGen / api /score_binding.py
TimelessAEther's picture
Upload ProRiboGen inference package and checkpoints
6dd9839 verified
Raw
History Blame Contribute Delete
6.28 kB
#!/usr/bin/env python3
"""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 # noqa: E402
from model import RnaRealismClassifierV3 # noqa: E402
from src.model import EsmConfig, EsmForMaskedLM # noqa: E402
from src.utils import _remap_legacy_checkpoint_state_dict, base_config # noqa: E402
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") # legacy env name
or "checkpoints/backbone.pt"
)
tok_dir = resolve(os.environ.get("TOKENIZER_DIR", "generator/tokenizer"))
bundle = torch.load(cls_ckpt, map_location="cpu")
# Checkpoints may still store config under the legacy key "d3lm_config".
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()