lnp_ml/tests/test_models.py

129 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""LNPModel 整图自检脚本
base/+moe 路径只需 rdkit + torch+llm 路径需 MolT5。
"""
from __future__ import annotations
import os
import sys
import traceback
from typing import Callable, Dict, List, Tuple
import torch
from lnp_ml.featurization.smiles import RDKitFeaturizer
from lnp_ml.modeling.models import LNPModelWithoutMPNN
MOLT5_PATH = os.environ.get("MOLT5_PATH", "models/molt5-base")
# 期望的任务输出维度
EXPECTED = {"size": 1, "pdi": 2, "ee": 3, "delivery": 1, "biodist": 7, "toxic": 2}
def _input_dims() -> Dict[str, int]:
"""根据实际 RDKit 输出确定无 MPNN 时的输入维度。"""
feat = RDKitFeaturizer().transform(["CCO"])
return {
"morgan": feat["morgan"].shape[1],
"maccs": feat["maccs"].shape[1],
"desc": feat["desc"].shape[1],
"comp": 5, "phys": 12, "help": 4, "exp": 32,
}
def _batch(B: int = 2) -> Tuple[List[str], Dict[str, torch.Tensor]]:
smiles = ["CCO", "CCN"][:B]
tabular = {
"comp": torch.randn(B, 5),
"phys": torch.randn(B, 12),
"help": torch.randn(B, 4),
"exp": torch.randn(B, 32),
}
return smiles, tabular
def _check_outputs(outputs: Dict[str, torch.Tensor], B: int) -> None:
for task, dim in EXPECTED.items():
assert outputs[task].shape == (B, dim), f"{task}: {outputs[task].shape} != ({B}, {dim})"
def test_forward_base() -> None:
model = LNPModelWithoutMPNN(d_model=64, input_dims=_input_dims()).eval()
smiles, tabular = _batch()
_check_outputs(model(smiles, tabular), B=2)
def test_forward_moe() -> None:
model = LNPModelWithoutMPNN(d_model=64, input_dims=_input_dims(), use_moe=True).eval()
smiles, tabular = _batch()
_check_outputs(model(smiles, tabular), B=2)
assert model.get_last_moe_extras() is not None, "use_moe=True 应产生 MoE extras"
def test_forward_from_projected() -> None:
model = LNPModelWithoutMPNN(d_model=64, input_dims=_input_dims()).eval()
smiles, tabular = _batch()
stacked = model._encode_and_project(smiles, tabular)
out = model.forward_from_projected(stacked, task="biodist")
assert out.shape == (2, 7), f"biodist {out.shape} != (2, 7)"
def test_split_idx_no_mpnn() -> None:
model = LNPModelWithoutMPNN(d_model=64, input_dims=_input_dims())
assert model.split_idx == 3, "无 MPNN 时化学 token 应为 3"
def test_forward_llm() -> None:
"""需要 MolT5缺失则跳过。"""
try:
model = LNPModelWithoutMPNN(
d_model=64, input_dims=_input_dims(),
use_moe=True, use_llm=True, llm_model_path=MOLT5_PATH,
).eval()
except Exception as e:
print(f"[SKIP] test_forward_llmMolT5 不可用):{e}")
return
smiles, tabular = _batch()
_check_outputs(model(smiles, tabular), B=2)
# 冻结的 MolT5 encoder 不应进入 backbone state dict
bb = model.get_backbone_state_dict()
assert not any(k.startswith("llm_prompt.encoder.") for k in bb), "backbone 不应含冻结 encoder 权重"
# ──────────────────────────────────────────────────────────────────────
# Runner
# ──────────────────────────────────────────────────────────────────────
def _collect_tests() -> List[Tuple[str, Callable[[], None]]]:
return [
(name, fn) for name, fn in globals().items()
if name.startswith("test_") and callable(fn)
]
def main() -> int:
tests = _collect_tests()
pass_count = 0
fail: List[Tuple[str, str]] = []
for name, fn in tests:
try:
fn()
except Exception:
fail.append((name, traceback.format_exc()))
print(f"[FAIL] {name}")
else:
pass_count += 1
print(f"[ OK ] {name}")
print(f"\n{'='*60}")
print(f"Passed: {pass_count}/{len(tests)}")
if fail:
print(f"\n{'='*60}\nFailures:")
for name, tb in fail:
print(f"\n--- {name} ---\n{tb}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())