Spaces:
Sleeping
Sleeping
File size: 14,858 Bytes
eb23a18 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | """Tests for v3.4.0: Biomni HPC backend fully removed; every step runs locally.
These tests assert that:
* the ``_hpc.py`` shim is gone and no module under ``core/`` imports ``biomni``
or calls ``hpc_run_tool`` / ``hpc_get_job_results`` / ``hpc_search_tools``;
* ``Boltz2Config`` no longer exposes ``use_hpc`` / ``hpc_tool_id`` and its
Boltz cache default is a user-home path (not an HPC-cluster path);
* the new local-run config fields exist (antibody RFAntibody dirs, ThermoMPNN);
* the CLI exposes and wires the new local flags;
* the local/skip/graft dispatch paths behave correctly on a box with no GPU,
no local binaries, and no API key (fold -> graft or skip, dock -> skip,
ddG -> ESM2 tier, antibody -> clean skip).
The suite is designed to pass on a CPU-only sandbox with none of the external
binaries (boltz, thermompnn, rfantibody, boltzgen) installed.
"""
import ast
import os
import glob
import importlib
import inspect
import tempfile
import pytest
import proteoform_analyzer
from proteoform_analyzer.core.config import (
Boltz2Config, AntibodyConfig, AnalysisConfig,
)
# Root of the installed package (…/proteoform_analyzer)
PKG_ROOT = os.path.dirname(os.path.abspath(proteoform_analyzer.__file__))
STEPS_DIR = os.path.join(PKG_ROOT, "core", "steps")
# ---------------------------------------------------------------------------
# 0. Version
# ---------------------------------------------------------------------------
def test_version_is_340():
assert proteoform_analyzer.__version__ == "3.4.0"
# ---------------------------------------------------------------------------
# 1. The HPC shim is deleted
# ---------------------------------------------------------------------------
def test_hpc_shim_deleted():
assert not os.path.exists(os.path.join(STEPS_DIR, "_hpc.py")), \
"core/steps/_hpc.py must be deleted in v3.4.0"
def test_hpc_shim_not_importable():
with pytest.raises(ModuleNotFoundError):
importlib.import_module("proteoform_analyzer.core.steps._hpc")
# ---------------------------------------------------------------------------
# 2. No active HPC / biomni code references anywhere under core/ (source scan)
# ---------------------------------------------------------------------------
# Substrings that would indicate a *live* HPC/biomni dependency. Historical
# changelog wording ("removed in v3.4.0") is allowed, so we scan for the actual
# call/import tokens rather than the word "HPC".
_FORBIDDEN_TOKENS = (
"hpc_run_tool",
"hpc_get_job_results",
"hpc_search_tools",
"hpc_get_logs",
"hpc_cancel_job",
"get_hpc_run_tool",
"get_hpc_get_job_results",
"HpcUnavailable",
"import biomni",
"from biomni",
"from ._hpc",
"import _hpc",
".hpc_tool_id",
"use_hpc",
)
def _py_files_under(root):
return glob.glob(os.path.join(root, "**", "*.py"), recursive=True)
def test_no_active_hpc_tokens_in_core():
core_dir = os.path.join(PKG_ROOT, "core")
offenders = {}
for fp in _py_files_under(core_dir):
with open(fp, "r", encoding="utf-8") as fh:
src = fh.read()
hits = [tok for tok in _FORBIDDEN_TOKENS if tok in src]
if hits:
offenders[os.path.relpath(fp, PKG_ROOT)] = hits
assert not offenders, f"Active HPC/biomni tokens found: {offenders}"
def test_no_active_hpc_tokens_in_cli_and_gui():
offenders = {}
for fp in (os.path.join(PKG_ROOT, "cli.py"), os.path.join(PKG_ROOT, "gui.py")):
with open(fp, "r", encoding="utf-8") as fh:
src = fh.read()
hits = [tok for tok in _FORBIDDEN_TOKENS if tok in src]
if hits:
offenders[os.path.basename(fp)] = hits
assert not offenders, f"Active HPC/biomni tokens found: {offenders}"
def test_biomni_not_imported_by_package_tree():
"""AST-level: no module in the package has a real `import biomni` statement."""
offenders = []
for fp in _py_files_under(PKG_ROOT):
# skip the test suite itself
if os.sep + "tests" + os.sep in fp:
continue
with open(fp, "r", encoding="utf-8") as fh:
try:
tree = ast.parse(fh.read())
except SyntaxError:
offenders.append((fp, "SYNTAX ERROR"))
continue
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for n in node.names:
if n.name.split(".")[0] == "biomni":
offenders.append((os.path.relpath(fp, PKG_ROOT), n.name))
elif isinstance(node, ast.ImportFrom):
if (node.module or "").split(".")[0] == "biomni":
offenders.append((os.path.relpath(fp, PKG_ROOT), node.module))
assert not offenders, f"biomni imports found: {offenders}"
# ---------------------------------------------------------------------------
# 3. Boltz2Config: HPC fields removed, cache default changed
# ---------------------------------------------------------------------------
def test_boltz2config_hpc_fields_removed():
b = Boltz2Config()
assert not hasattr(b, "use_hpc"), "Boltz2Config.use_hpc must be removed"
assert not hasattr(b, "hpc_tool_id"), "Boltz2Config.hpc_tool_id must be removed"
def test_boltz2config_rejects_hpc_kwargs():
with pytest.raises(TypeError):
Boltz2Config(use_hpc=True)
with pytest.raises(TypeError):
Boltz2Config(hpc_tool_id="boltz-2")
def test_boltz2config_cache_default_is_home():
b = Boltz2Config()
# Must no longer point at the old HPC-cluster path.
assert "/mnt/fsx" not in b.cache_dir
assert b.cache_dir == os.path.expanduser("~/.cache/boltz")
# ---------------------------------------------------------------------------
# 4. New local-run config fields
# ---------------------------------------------------------------------------
def test_antibody_local_fields_exist():
a = AntibodyConfig()
for f in ("local_rfantibody_dir", "local_weights_dir",
"local_python", "local_framework_pdb"):
assert hasattr(a, f), f"AntibodyConfig.{f} missing"
assert getattr(a, f) is None # default: unconfigured
def test_thermompnn_config_fields_exist():
c = AnalysisConfig()
for f in ("thermompnn_dir", "thermompnn_script",
"thermompnn_checkpoint", "thermompnn_python"):
assert hasattr(c, f), f"AnalysisConfig.{f} missing"
assert getattr(c, f) is None
# ---------------------------------------------------------------------------
# 5. CLI exposes + wires the new local flags
# ---------------------------------------------------------------------------
def test_cli_has_new_local_flags():
from proteoform_analyzer.cli import build_parser
parser = build_parser()
help_txt = parser.format_help() if hasattr(parser, "format_help") else ""
# Parse a representative command line and confirm the fields land on args.
args = parser.parse_args([
"run", "--uniprot", "P69905", "--n-subunits", "1",
"--antibody",
"--antibody-rfantibody-dir", "/tmp/rfab",
"--antibody-weights-dir", "/tmp/w",
"--thermompnn-dir", "/tmp/tmpnn",
"--thermompnn-checkpoint", "/tmp/ckpt.pt",
])
assert args.antibody_rfantibody_dir == "/tmp/rfab"
assert args.antibody_weights_dir == "/tmp/w"
assert args.thermompnn_dir == "/tmp/tmpnn"
assert args.thermompnn_checkpoint == "/tmp/ckpt.pt"
def test_cli_structure_source_help_has_no_hpc():
from proteoform_analyzer.cli import build_parser
parser = build_parser()
txt = parser.format_help()
# The word "HPC" must not survive in the run-subcommand help text.
# (We check the subparser help by formatting the full parser tree.)
assert "HPC" not in txt
# ---------------------------------------------------------------------------
# 6. Folding dispatch: graft / skip on a bare box
# ---------------------------------------------------------------------------
def _bare_cfg(monkeypatch, **boltz_kw):
"""AnalysisConfig with a Boltz2Config; helper for dispatch tests."""
ac = AnalysisConfig(uniprot_ids=["P69905"], n_subunits=1)
ac.boltz2 = Boltz2Config(**boltz_kw)
return ac
def test_fold_dispatch_functions_present():
from proteoform_analyzer.core.steps import boltz2_fold as bf
# Local + graft dispatch helpers must exist; HPC helpers must be gone.
for fn in ("_fold_local", "_fold_graft", "_fold_api", "build_structures",
"_resolve_local_binary", "_run_local_boltz"):
assert hasattr(bf, fn), f"missing {fn}"
for gone in ("_submit_hpc", "_hpc_command", "_try_collect_job",
"_manifest_path", "_load_manifest", "_save_manifest"):
assert not hasattr(bf, gone), f"{gone} should have been deleted"
def test_build_structures_skips_cleanly_without_backend(monkeypatch):
"""No API key, no local binary, graft disabled -> clean skip (not crash)."""
from proteoform_analyzer.core.steps import boltz2_fold as bf
from proteoform_analyzer.core.steps import _boltz_backend as bb
monkeypatch.setattr(bb, "resolve_backend", lambda cfg, kind: "none")
cfg = _bare_cfg(monkeypatch, allow_graft_fallback=False)
with tempfile.TemporaryDirectory() as tmp:
paths = {k: os.path.join(tmp, k) for k in ("pdbs", "pdbs_monomer")}
for d in paths.values():
os.makedirs(d, exist_ok=True)
res = bf.build_structures(cfg, paths)
assert res.status in ("skipped", "ok")
# On a bare box with backend 'none', it must not be 'failed'.
assert res.status != "failed", res.message
# ---------------------------------------------------------------------------
# 7. Docking: clean skip when backend resolves to none
# ---------------------------------------------------------------------------
def test_docking_boltz2_skips_cleanly_without_backend(monkeypatch):
from proteoform_analyzer.core.steps import docking as dk
from proteoform_analyzer.core.steps import _boltz_backend as bb
# Ensure the boltz2 docking path resolves to 'none' and no local binary.
monkeypatch.setattr(bb, "resolve_backend", lambda cfg, kind: "none")
# _run_boltz2_dock is the internal entry; assert it exists and skips.
assert hasattr(dk, "_run_boltz2_dock")
src = inspect.getsource(dk)
# The HPC submit branch must be gone from the docking source.
for tok in ("hpc_run_tool", "get_hpc_run_tool", "hpc_tool_id"):
assert tok not in src, f"docking.py still references {tok}"
# ---------------------------------------------------------------------------
# 8. ddG: ThermoMPNN local resolver + ESM2 fallback structure
# ---------------------------------------------------------------------------
def test_ddg_thermompnn_resolver_returns_none_when_unconfigured():
from proteoform_analyzer.core.steps import ddg
cfg = AnalysisConfig() # thermompnn_* all None
assert ddg._resolve_thermompnn(cfg) is None
def test_ddg_thermompnn_resolver_needs_checkpoint(tmp_path):
"""A dir with a script but no checkpoint must not resolve (falls back to ESM2)."""
from proteoform_analyzer.core.steps import ddg
tdir = tmp_path / "thermompnn"
tdir.mkdir()
(tdir / "custom_inference.py").write_text("# stub\n")
cfg = AnalysisConfig(thermompnn_dir=str(tdir)) # no checkpoint
assert ddg._resolve_thermompnn(cfg) is None
def test_ddg_has_esm2_fallback_and_local_thermompnn():
from proteoform_analyzer.core.steps import ddg
for fn in ("_resolve_thermompnn", "_thermompnn_for_chain_local",
"_run_thermompnn", "_esm2_zeroshot_ddg", "_run_esm2_zeroshot",
"run_ddg"):
assert hasattr(ddg, fn), f"missing {fn}"
# The old HPC ThermoMPNN submit helper must be gone.
assert not hasattr(ddg, "_thermompnn_for_chain"), \
"_thermompnn_for_chain (HPC submit) should have been replaced"
# ddg must not import biomni / call hpc.
src = inspect.getsource(ddg)
for tok in ("hpc_run_tool", "biomni", "get_hpc_run_tool"):
assert tok not in src, f"ddg.py still references {tok}"
# ---------------------------------------------------------------------------
# 9. Antibody: local RFAntibody or clean skip (no HPC)
# ---------------------------------------------------------------------------
def test_antibody_no_hpc_helpers():
from proteoform_analyzer.core.steps import antibody as ab
for gone in ("_load_manifest", "_save_manifest", "_manifest_path",
"_submit", "_hpc_command"):
assert not hasattr(ab, gone), f"{gone} should have been deleted"
for fn in ("_resolve_local_rfantibody", "_stage1_cmd", "_stage2_cmd",
"_stage3_cmd", "_run_stage", "run_antibody"):
assert hasattr(ab, fn), f"missing {fn}"
src = inspect.getsource(ab)
for tok in ("hpc_run_tool", "biomni", "get_hpc_run_tool", "HpcUnavailable"):
assert tok not in src, f"antibody.py still references {tok}"
def test_antibody_resolver_none_when_unconfigured():
from proteoform_analyzer.core.steps.antibody import _resolve_local_rfantibody
cfg = AnalysisConfig()
cfg.antibody.enabled = True
# local_rfantibody_dir is None -> no runnable install
assert _resolve_local_rfantibody(cfg) is None
def test_antibody_skips_cleanly_without_local_install(tmp_path):
"""Enabled antibody step with a target but no local RFAntibody -> clean skip."""
from proteoform_analyzer.core.steps.antibody import run_antibody
paths = {k: str(tmp_path / k) for k in ("antibody", "pdbs", "pdbs_monomer")}
for d in paths.values():
os.makedirs(d, exist_ok=True)
# Minimal WT monomer PDB (two CA atoms, chain A).
pdb = os.path.join(paths["pdbs_monomer"], "wt-monomer.pdb")
with open(pdb, "w") as f:
f.write("ATOM 1 CA ALA A 305 11.000 11.000 11.000 1.00 0.00 C\n")
f.write("ATOM 2 CA GLY A 306 15.000 15.000 15.000 1.00 0.00 C\n")
f.write("END\n")
cfg = AnalysisConfig(uniprot_ids=["P69905"], n_subunits=1)
cfg.antibody.enabled = True
cfg.antibody.hotspot_source = "user"
cfg.antibody.hotspot_residues = ["305"]
res = run_antibody(cfg, paths)
assert res.status == "skipped"
assert "local RFAntibody" in res.message
# No design_context.json is written when we skip before provenance.
assert not os.path.exists(os.path.join(paths["antibody"], "design_context.json"))
def test_antibody_disabled_skips():
from proteoform_analyzer.core.steps.antibody import run_antibody
cfg = AnalysisConfig()
with tempfile.TemporaryDirectory() as tmp:
res = run_antibody(cfg, {"antibody": os.path.join(tmp, "ab")})
assert res.status == "skipped"
assert "not enabled" in res.message
|