From 5617c237991445ed98a0c5e3060a694a6f6bdf30 Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Thu, 27 Aug 2026 10:01:20 -0500 Subject: [PATCH 01/12] few updates to run with Dragon and remove hardcoded paths from code --- .gitignore | 5 + examples/protien_binding_usecase/SKILL.md | 131 +++++++++ .../af2_multimer_reduced.sh | 12 +- .../delta_env_setup.sh | 163 +++++++++++ .../protien_binding_usecase/delta_gpu_run.sh | 44 +++ .../run_protein_binding.py | 57 +++- src/impress/pipelines/protein_binding.py | 261 +++++++++++------- 7 files changed, 553 insertions(+), 120 deletions(-) create mode 100644 examples/protien_binding_usecase/SKILL.md create mode 100644 examples/protien_binding_usecase/delta_env_setup.sh create mode 100644 examples/protien_binding_usecase/delta_gpu_run.sh diff --git a/.gitignore b/.gitignore index cca190f..0ed6970 100644 --- a/.gitignore +++ b/.gitignore @@ -135,6 +135,11 @@ dmypy.json # asyncflow related asyncflow.session.* +ddict_* + +# ROME runtime outputs +af_stats_*.csv +examples/protien_binding_usecase/logs/ # disco wf outputs b0 diff --git a/examples/protien_binding_usecase/SKILL.md b/examples/protien_binding_usecase/SKILL.md new file mode 100644 index 0000000..e05135c --- /dev/null +++ b/examples/protien_binding_usecase/SKILL.md @@ -0,0 +1,131 @@ +# Protein Binding Use Case — SKILL.md + +## What it does + +5-step adaptive protein design pipeline running on Delta HPC (gpuA40x4): + +| Step | Task | Type | Notes | +|------|------|------|-------| +| s1 | ProteinMPNN — generate sequences | local (subprocess) | GPU via `CUDA_VISIBLE_DEVICES` | +| s2 | Sequence ranking | local (pure Python) | sorts by MPNN score | +| s3 | FASTA generation | local (pure Python) | writes `.fa` for each target | +| s4 | AlphaFold multimer prediction | local (subprocess) | GPU, runs via Apptainer | +| s4_post | Copy best models | local (pure Python) | glob → shutil.copy | +| s5 | pLDDT extraction | local (subprocess) | writes `af_stats__pass_.csv` | + +Passes repeat up to `max_passes` times. The adaptive function reads pLDDT CSVs and can spawn child pipelines for proteins that degrade. + +--- + +## Key files + +``` +protien_binding_usecase/ +├── run_protein_binding.py # entry point — backend, GPU policy, ImpressManager +├── delta_gpu_run.sh # SLURM script — Dragon launcher +├── delta_env_setup.sh # one-time venv setup (~/ve/impress/) +├── af2_multimer_reduced.sh # Apptainer wrapper for AlphaFold multimer +├── mpnn_wrapper.py # ProteinMPNN CLI wrapper +└── plddt_extract_pipeline.py # reads AF JSON → writes CSV +``` + +Pipeline implementation: `/scratch/bblj/mgoliyad1/IMPRESS/src/impress/pipelines/protein_binding.py` + +--- + +## Environment variables + +| Variable | Set in | Purpose | +|----------|--------|---------| +| `SCRATCH` | user env | base scratch path (`/scratch/bblj`) | +| `MPNN_PATH` | `delta_gpu_run.sh` | path to ProteinMPNN repo | +| `AF2_DATABASE` | `delta_gpu_run.sh` | AlphaFold database dir | +| `AF2_SIF` | `delta_gpu_run.sh` | AlphaFold Apptainer image | +| `IMPRESS_INPUT_DIR` | `delta_gpu_run.sh` | dir containing `{name}_in/` folders | +| `IMPRESS_OUTPUT_DIR` | `delta_gpu_run.sh` | output root (af_pipeline_outputs_multi/) | +| `IMPRESS_SCRIPTS_DIR` | `delta_gpu_run.sh` | dir with mpnn_wrapper.py, af2_multimer_reduced.sh | +| `SBATCH_ACCOUNT` | user env | SLURM account (e.g. `bblj-delta-gpu`) | + +`IMPRESS_PRE_EXEC` was removed — the venv is activated in the SLURM script before `dragon -s`, so all subprocesses inherit the environment. + +--- + +## Dragon integration + +**Backend**: `DragonExecutionBackendV2` (flow engine only — no tasks dispatched to Dragon workers). + +**Why not V3**: Dragon worker subprocesses silently hang when a subprocess uses GPU ops (`cupy`, `torch`, Apptainer). Same failure mode documented in `DeepDriveSim/workflows/miniapps_workflow/miniapps_workflow.py`. + +**Fix**: All pipeline steps are `local_task=True`. GPU steps (s1, s4) run as `asyncio.create_subprocess_shell()` on the Dragon head process, which has direct GPU access from SLURM allocation. Dragon manages the `ImpressManager`/`WorkflowEngine` loop; it never dispatches tasks to workers. + +**GPU affinity**: `_find_gpus()` + `_make_policy()` in `run_protein_binding.py` use `dragon.native.machine.System()` to discover GPUs and build a `dragon.infrastructure.policy.Policy` with `gpu_affinity`. The policy's GPU list is passed as `CUDA_VISIBLE_DEVICES` to each subprocess via `_gpu_env()` in the pipeline. + +```python +# To change GPU count per pipeline (default n_gpus=1 for MPNN): +policy = _make_policy(all_gpus, idx=0, n_gpus=1) +``` + +**Launcher** (`delta_gpu_run.sh`): +```bash +source ~/ve/impress/bin/activate +dragon-config add --ofi-runtime-lib="${FAB_LIB}" +rm -rf asyncflow.session.* +dragon -s run_protein_binding.py # -s = single-node +``` + +**SLURM resource request**: `--tasks-per-node=4`, `--gpus=4`, `--exclusive` (matches Dragon's single-node layout). + +--- + +## Key design decisions + +### Three separate path env vars +`base_path` was split into `IMPRESS_INPUT_DIR` / `IMPRESS_OUTPUT_DIR` / `IMPRESS_SCRIPTS_DIR` so input data, outputs, and scripts can live in different locations independently. + +### Output dirs auto-created in `__init__` +`os.makedirs(..., exist_ok=True)` is called in `ProteinBindingPipeline.__init__` for all required subdirs. No manual `mkdir` needed in the SLURM script. + +### `s4_post` replaces `post_exec` +`post_exec` in task descriptions is a RADICAL-Pilot artifact — `ConcurrentExecutionBackend` and Dragon never execute it. Replaced with a `local_task=True` coroutine using `glob` + `shutil.copy` to move `ranked_0.pdb` and `ranking_debug.json` to `best_models/` and `best_ptm/`. + +### AlphaFold container flags +- `--no-home`: prevents host's newer Biopython from shadowing the container's version (`SCOPData` import error) +- `--run_relax` removed: not supported by this container version +- Database bind: `/scratch/rhaas/SUP-5301/database:/database` +- Container: `/scratch/rhaas/SUP-5301/alphafold.sif` + +--- + +## One-time setup + +```bash +export SCRATCH=/scratch/bblj +cd /scratch/bblj/mgoliyad1/IMPRESS/examples/protien_binding_usecase +bash delta_env_setup.sh # creates ~/ve/impress/ + +# Unzip inputs (GNU tar, not zip): +cd /scratch/bblj/mgoliyad1/IMPRESS_inputs +tar -xf prod_in.tar # or whatever archive name +``` + +## Submitting a job + +```bash +export SCRATCH=/scratch/bblj +export SBATCH_ACCOUNT=bblj-delta-gpu +cd /scratch/bblj/mgoliyad1/IMPRESS/examples/protien_binding_usecase +sbatch delta_gpu_run.sh +``` + +## Checking results + +```bash +# pLDDT scores per pass: +cat af_stats_p1_pass_1.csv + +# AlphaFold best models: +ls IMPRESS_outputs/af_pipeline_outputs_multi/p1/af/prediction/best_models/ + +# Full logs: +tail -f logs/impress_.out +``` diff --git a/examples/protien_binding_usecase/af2_multimer_reduced.sh b/examples/protien_binding_usecase/af2_multimer_reduced.sh index ac686ce..3091556 100644 --- a/examples/protien_binding_usecase/af2_multimer_reduced.sh +++ b/examples/protien_binding_usecase/af2_multimer_reduced.sh @@ -21,11 +21,14 @@ INPUT_FASTA_FILE_DIR=$1 INPUT_FASTA_FILE_NAME=$2 OUTPUT_DATA_DIR=$3 -apptainer run --nv \ +: "${AF2_DATABASE:?AF2_DATABASE is not set (path to AlphaFold database dir)}" +: "${AF2_SIF:?AF2_SIF is not set (path to alphafold.sif container)}" + +apptainer run --nv --no-home \ --bind $INPUT_FASTA_FILE_DIR:/fasta \ --bind $OUTPUT_DATA_DIR:/dimer_models \ - --bind /anvil/datasets/alphafold/db_20230311:/database \ - /apps/biocontainers/images/tacc_alphafold:2.3.1.sif \ + --bind ${AF2_DATABASE}:/database \ + ${AF2_SIF} \ --data_dir=/database \ --uniref90_database_path=/database/uniref90/uniref90.fasta \ --mgnify_database_path=/database/mgnify/mgy_clusters_2022_05.fa \ @@ -40,5 +43,4 @@ apptainer run --nv \ --pdb_seqres_database_path=/database/pdb_seqres/pdb_seqres.txt \ --max_template_date=2020-12-01 \ --use_gpu_relax=False \ - --num_multimer_predictions_per_model=1 \ - --run_relax=False \ No newline at end of file + --num_multimer_predictions_per_model=1 \ No newline at end of file diff --git a/examples/protien_binding_usecase/delta_env_setup.sh b/examples/protien_binding_usecase/delta_env_setup.sh new file mode 100644 index 0000000..9abcad1 --- /dev/null +++ b/examples/protien_binding_usecase/delta_env_setup.sh @@ -0,0 +1,163 @@ +#!/bin/bash +# ============================================================================= +# IMPRESS Protein Binding environment setup — Delta HPC (NCSA) +# +# Creates a Python 3.11+ venv and installs all dependencies. +# +# Usage: +# export SCRATCH=/scratch/ +# bash delta_env_setup.sh [--env-dir DIR] [--impress-dir DIR] [--python PATH] +# +# Defaults: +# ENV_DIR = /u/$USER/ve/impress +# IMPRESS_DIR = $SCRATCH/$USER/IMPRESS +# python = auto-detected (python/3.11, cray-python/3.11.7, anaconda3) +# ============================================================================= +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + set -euo pipefail +fi + +# ── Require SCRATCH ─────────────────────────────────────────────────────────── +if [[ -z "${SCRATCH:-}" ]]; then + echo "ERROR: set the SCRATCH env var to your allocation scratch root, e.g.:" + echo " export SCRATCH=/scratch/" + echo " bash delta_env_setup.sh" + exit 1 +fi + +# ── Defaults / arg parsing ──────────────────────────────────────────────────── +ENV_DIR="${ENV_DIR:-/u/${USER}/ve/impress}" +IMPRESS_DIR="${IMPRESS_DIR:-${SCRATCH}/${USER}/IMPRESS}" +BASE_PY_OVERRIDE="" + +while [[ $# -gt 0 ]]; do + case $1 in + --env-dir) ENV_DIR="$2"; shift 2 ;; + --impress-dir) IMPRESS_DIR="$2"; shift 2 ;; + --python) BASE_PY_OVERRIDE="$2"; shift 2 ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done + +PY="${ENV_DIR}/bin/python" +PIP="${ENV_DIR}/bin/pip" + +echo "=================================================================" +echo " ENV_DIR = ${ENV_DIR}" +echo " IMPRESS_DIR = ${IMPRESS_DIR}" +echo "=================================================================" + +# ── 1. Create venv ──────────────────────────────────────────────────────────── +echo "" +echo "── Step 1: Creating venv ──" + +_find_python() { + for candidate in python3.12 python3.11 python3 python; do + local p + p=$(command -v "${candidate}" 2>/dev/null) || continue + local ver + ver=$("${p}" -c "import sys; v=sys.version_info; print(v.major*10+v.minor)" 2>/dev/null) || continue + [ "${ver}" -ge 311 ] && echo "${p}" && return 0 + done + return 1 +} + +if [ -n "${BASE_PY_OVERRIDE}" ]; then + BASE_PY="${BASE_PY_OVERRIDE}" + echo "Using Python override: ${BASE_PY}" +else + BASE_PY=$(_find_python || true) + if [ -z "${BASE_PY}" ]; then + echo "python3.11+ not in PATH — trying modules..." + for mod in python/3.12 python/3.11 cray-python/3.11.7 anaconda3; do + module load "${mod}" 2>/dev/null || true + BASE_PY=$(_find_python || true) + [ -n "${BASE_PY}" ] && echo " loaded module: ${mod}" && break + done + fi + if [ -z "${BASE_PY}" ]; then + echo "ERROR: no Python 3.11+ interpreter found." + echo " Pass an explicit interpreter: --python /path/to/python3.11" + echo " Or load a module manually before running this script." + exit 1 + fi +fi +echo "Using Python: ${BASE_PY} ($(${BASE_PY} --version))" + +if [ ! -x "${PY}" ]; then + "${BASE_PY}" -m venv "${ENV_DIR}" +else + echo "venv already exists at ${ENV_DIR}" +fi + +echo "Python: $("${PY}" --version)" + +# ── 2. Bootstrap pip ────────────────────────────────────────────────────────── +echo "" +echo "── Step 2: Bootstrapping pip ──" +"${PY}" -m pip install -q --upgrade pip wheel +"${PIP}" install -q --force-reinstall "setuptools<71" + +# ── 3. radical.asyncflow (PyPI) ────────────────────────────────────────────── +echo "" +echo "── Step 3: radical-asyncflow (PyPI) ──" +"${PIP}" install -q radical-asyncflow + +# ── 4. rhapsody-py (PyPI) ──────────────────────────────────────────────────── +echo "" +echo "── Step 4: rhapsody-py[dragon] (PyPI) ──" +"${PIP}" install -q "rhapsody-py[dragon,telemetry]" + +# ── 5. IMPRESS (local editable) ─────────────────────────────────────────────── +echo "" +echo "── Step 5: IMPRESS (editable) ──" +"${PIP}" install -q -e "${IMPRESS_DIR}" + +# ── 6. PyTorch (CUDA 12.1) ─────────────────────────────────────────────────── +echo "" +echo "── Step 6: PyTorch (cu121) ──" +"${PIP}" install -q torch --index-url https://download.pytorch.org/whl/cu121 + +# ── 7. Additional dependencies ─────────────────────────────────────────────── +echo "" +echo "── Step 7: pandas + biopandas ──" +"${PIP}" install -q pandas biopandas + +# ── 8. PyRosetta (via pyrosetta-installer) ─────────────────────────────────── +echo "" +echo "── Step 8: PyRosetta ──" +"${PIP}" install -q pyrosetta-installer +"${PY}" -c "import pyrosetta_installer; pyrosetta_installer.install_pyrosetta()" + +# ── 9. Verify ──────────────────────────────────────────────────────────────── +echo "" +echo "── Step 9: Verifying installation ──" +_check() { + local label="$1"; shift + if out=$("$@" 2>&1); then + echo " ${label}: OK (${out})" + else + echo " WARNING: ${label} failed" + echo " ${out}" | head -3 + fi +} + +_check "radical.asyncflow" "${PY}" -c "import radical.asyncflow; print(radical.asyncflow.__version__)" +_check "rhapsody-py" "${PY}" -c "import rhapsody; print('ok')" +_check "impress" "${PY}" -c "import impress; print('ok')" +_check "torch" "${PY}" -c "import torch; print(torch.__version__)" +_check "pandas" "${PY}" -c "import pandas; print(pandas.__version__)" + +echo "" +echo "=================================================================" +echo "Setup complete." +echo "" +echo "Activate with:" +echo " source ${ENV_DIR}/bin/activate" +echo "" +echo "Run the pipeline:" +echo " export SCRATCH=${SCRATCH}" +echo " export SBATCH_ACCOUNT=bblj-delta-gpu" +echo " cd ${IMPRESS_DIR}/examples/protien_binding_usecase" +echo " sbatch delta_gpu_run.sh" +echo "=================================================================" diff --git a/examples/protien_binding_usecase/delta_gpu_run.sh b/examples/protien_binding_usecase/delta_gpu_run.sh new file mode 100644 index 0000000..d69a637 --- /dev/null +++ b/examples/protien_binding_usecase/delta_gpu_run.sh @@ -0,0 +1,44 @@ +#!/bin/bash +#SBATCH --partition=gpuA40x4 +#SBATCH --nodes=1 +#SBATCH --tasks-per-node=4 +#SBATCH --cpus-per-task=16 +#SBATCH --gpus=4 +#SBATCH --exclusive +#SBATCH --time=00:30:00 +#SBATCH --job-name=impress_protein +#SBATCH --mail-user=mariya.goliyad@rutgers.edu +#SBATCH --mail-type=END,FAIL +#SBATCH --output=logs/impress_%j.out +#SBATCH --error=logs/impress_%j.err + +set -e + +# ── System library paths (Delta-specific, required by Dragon) ───────────────── +export CUDA_HOME=/opt/nvidia/hpc_sdk/Linux_x86_64/25.3/cuda/12.8 +export MPI_LIB=/opt/cray/pe/mpich/8.1.32/ofi/gnu/11.2/lib-abi-mpich +export FAB_LIB=/opt/cray/libfabric/1.22.0/lib64 +export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH} + +# ── Paths — edit these when moving to a different system ───────────────────── +: "${SCRATCH:?SCRATCH is not set (e.g. export SCRATCH=/scratch/bblj)}" +export MPNN_PATH="$SCRATCH/$USER/ProteinMPNN" +# Switch activation command below if using conda instead of venv +IMPRESS_PRE_EXEC="source $HOME/ve/impress/bin/activate" +export AF2_DATABASE="/scratch/rhaas/SUP-5301/database" +export AF2_SIF="/scratch/rhaas/SUP-5301/alphafold.sif" +export IMPRESS_INPUT_DIR="$SCRATCH/$USER/IMPRESS_inputs/prod_in" +export IMPRESS_OUTPUT_DIR="$SCRATCH/$USER/IMPRESS_outputs" +export IMPRESS_SCRIPTS_DIR="$SCRATCH/$USER/IMPRESS/examples/protien_binding_usecase" + +# ── Working directory ───────────────────────────────────────────────────────── +WORKDIR="$SCRATCH/$USER/IMPRESS/examples/protien_binding_usecase" +cd "$WORKDIR" +mkdir -p logs + +eval "$IMPRESS_PRE_EXEC" +dragon-config add --ofi-runtime-lib="${FAB_LIB}" + +# ── Run ─────────────────────────────────────────────────────────────────────── +rm -rf asyncflow.session.* +dragon -s run_protein_binding.py diff --git a/examples/protien_binding_usecase/run_protein_binding.py b/examples/protien_binding_usecase/run_protein_binding.py index a3b9303..a5cf522 100644 --- a/examples/protien_binding_usecase/run_protein_binding.py +++ b/examples/protien_binding_usecase/run_protein_binding.py @@ -3,7 +3,9 @@ import asyncio from typing import Dict, Any, Optional, List -from radical.asyncflow import RadicalExecutionBackend +from rhapsody.backends import DragonExecutionBackend +from rhapsody.backends import ConcurrentExecutionBackend +from concurrent.futures import ProcessPoolExecutor from impress import PipelineSetup from impress import ImpressManager @@ -84,7 +86,7 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[s # Copy PDB files for bad proteins for protein in sub_iter_seqs: src = f'{pipeline.output_path_af}/{protein}.pdb' - dst = f'{pipeline.base_path}/{new_name}_in/{protein}.pdb' + dst = f'{pipeline.input_base_path}/{new_name}_in/{protein}.pdb' shutil.copyfile(src, dst) # Build a request for a new pipeline @@ -114,30 +116,59 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[s pipeline.previous_scores = copy.deepcopy(pipeline.current_scores) +def _find_gpus(): + """Return [(hostname, gpu_id), ...] for every GPU visible to Dragon.""" + import socket + from dragon.native.machine import Node, System + real_hostname = socket.gethostname() + all_gpus = [] + for huid in System().nodes: + node = Node(huid) + hostname = node.hostname if node.hostname != "localhost" else real_hostname + for gpu_id in node.gpus or []: + all_gpus.append((hostname, gpu_id)) + return all_gpus + + +def _make_policy(all_gpus, idx=0, n_gpus=1): + """Create a Dragon Policy with gpu_affinity for n_gpus consecutive GPU slots.""" + if not all_gpus: + print("WARNING: no GPUs found via Dragon — running without GPU affinity policy") + return None + from dragon.infrastructure.policy import Policy + hostname = all_gpus[idx % len(all_gpus)][0] + gpu_ids = [all_gpus[(idx + i) % len(all_gpus)][1] for i in range(n_gpus)] + return Policy( + placement=Policy.Placement.HOST_NAME, + host_name=hostname, + gpu_affinity=gpu_ids, + ) + + async def impress_protein_bind() -> None: """ Execute protein binding analysis with adaptive optimization. - + Creates and manages multiple ProteinBindingPipeline instances with adaptive optimization capabilities. Each pipeline can spawn child pipelines based on protein quality degradation. """ - backend = await RadicalExecutionBackend( - { - 'gpus':1, - 'cores': 32, - 'runtime' : 23 * 60, - 'resource': 'purdue.anvil_gpu' - } - ) + import os + if os.environ.get("IMPRESS_BACKEND", "dragon").lower() == "concurrent": + backend = await ConcurrentExecutionBackend(ProcessPoolExecutor()) + all_gpus = [] + else: + backend = await DragonExecutionBackend() + all_gpus = _find_gpus() + policy = _make_policy(all_gpus, idx=0, n_gpus=1) manager: ImpressManager = ImpressManager(execution_backend=backend) - pipeline_setups: List[PipelineSetup] = [ PipelineSetup( name='p1', type=ProteinBindingPipeline, - adaptive_fn=adaptive_decision + adaptive_fn=adaptive_decision, + kwargs={"policy": policy}, ) ] diff --git a/src/impress/pipelines/protein_binding.py b/src/impress/pipelines/protein_binding.py index ec3b3de..5565ea3 100644 --- a/src/impress/pipelines/protein_binding.py +++ b/src/impress/pipelines/protein_binding.py @@ -4,13 +4,11 @@ from .impress_pipeline import ImpressBasePipeline -TASK_PRE_EXEC = [ - "module load anaconda", - "source activate base", - (f"conda activate /anvil/scratch/{os.environ['USER']}/impress/ve.impress"), -] +_mpnn = os.environ.get("MPNN_PATH") +if not _mpnn: + raise EnvironmentError("MPNN_PATH is not set (path to the ProteinMPNN repo)") -MPNN_PATH = f"/anvil/scratch/{os.environ['USER']}/impress/ProteinMPNN" +MPNN_PATH = _mpnn class ProteinBindingPipeline(ImpressBasePipeline): @@ -19,50 +17,89 @@ def __init__(self, name, flow, configs=None, **kwargs): if configs is None: configs = {} - self.is_child: bool = kwargs.get("is_child", False) - self.passes = kwargs.get("passes", 1) - self.start_pass: int = kwargs.get("start_pass", 1) - self.step_id = kwargs.get("step_id", 1) - self.seq_rank = kwargs.get("seq_rank", 0) - self.num_seqs = kwargs.get("num_seqs", 10) - self.sub_order = kwargs.get("sub_order", 0) - self.max_passes = kwargs.get("max_passes", 4) - self.mpnn_path = kwargs.get("mpnn_path", MPNN_PATH) + # Child pipelines receive state in `configs`; top-level pipelines in `**kwargs`. + # Check kwargs first so callers can override any config key. + def _cfg(key, default): + if key in kwargs: + return kwargs[key] + if key in configs: + return configs[key] + return default + + self.is_child: bool = _cfg("is_child", False) + self.passes = _cfg("passes", 1) + self.start_pass: int = _cfg("start_pass", 1) + self.step_id = _cfg("step_id", 1) + self.seq_rank = _cfg("seq_rank", 0) + self.num_seqs = _cfg("num_seqs", 10) + self.sub_order = _cfg("sub_order", 0) + self.max_passes = _cfg("max_passes", int(os.environ.get("ROME_MAX_PASSES", 10))) + self.mpnn_path = _cfg("mpnn_path", MPNN_PATH) + self.policy = _cfg("policy", None) # Sequence and score state self.current_scores = {} - self.iter_seqs = kwargs.get("iter_seqs", {}) - self.previous_scores = kwargs.get("previous_score", {}) + self.iter_seqs = _cfg("iter_seqs", {}) + self.previous_scores = _cfg("previous_scores", {}) - super().__init__(name, flow, **configs, **kwargs) + # Exclude from configs any keys already in kwargs to prevent duplicate-keyword TypeError. + filtered_configs = {k: v for k, v in configs.items() if k not in kwargs} + super().__init__(name, flow, **filtered_configs, **kwargs) - # Input-related self.fasta_list_2 = kwargs.get("fasta_list_2", []) - self.base_path = kwargs.get("base_path", os.getcwd()) - self.input_path = os.path.join(self.base_path, f"{self.name}_in") - # Output paths - self.output_path = os.path.join( - self.base_path, "af_pipeline_outputs_multi", self.name + # Separate base directories — kwargs take priority (child pipelines), + # env vars are the default for top-level pipelines. + self.input_base_path = kwargs.get( + "input_base_path", os.environ.get("IMPRESS_INPUT_DIR", "") ) - self.output_path_mpnn = os.path.join(self.output_path, "mpnn") - self.output_path_af = os.path.join( - self.output_path, "af/prediction/best_models" + self.output_base_path = kwargs.get( + "output_base_path", os.environ.get("IMPRESS_OUTPUT_DIR", "") + ) + self.scripts_path = kwargs.get( + "scripts_path", os.environ.get("IMPRESS_SCRIPTS_DIR", "") ) - # might have to do outside of initialization, so new pipelines - # do not run this can be declared directly as argument + if not self.input_base_path: + raise EnvironmentError(f"IMPRESS_INPUT_DIR is not set (dir containing {name}_in/ folders)") + if not self.output_base_path: + raise EnvironmentError("IMPRESS_OUTPUT_DIR is not set (dir for af_pipeline_outputs_multi/)") + if not self.scripts_path: + raise EnvironmentError("IMPRESS_SCRIPTS_DIR is not set (dir with mpnn_wrapper.py and af2_multimer_reduced.sh)") + + self.input_path = os.path.join(self.input_base_path, f"{self.name}_in") + self.output_path = os.path.join(self.output_base_path, "af_pipeline_outputs_multi", self.name) + self.output_path_mpnn = os.path.join(self.output_path, "mpnn") + self.output_path_af = os.path.join(self.output_path, "af/prediction/best_models") + + for subdir in [ + "af/fasta", + "af/prediction/best_models", + "af/prediction/best_ptm", + "af/prediction/dimer_models", + "af/prediction/logs", + *[f"mpnn/job_{i}" for i in range(1, self.max_passes + 1)], + ]: + os.makedirs(os.path.join(self.output_path, subdir), exist_ok=True) + for file_name in os.listdir(self.input_path): self.fasta_list_2.append(file_name) + def _gpu_env(self): + """Return subprocess env with CUDA_VISIBLE_DEVICES set from policy gpu_affinity.""" + env = {**os.environ} + if self.policy and self.policy.gpu_affinity: + env["CUDA_VISIBLE_DEVICES"] = ",".join(str(g) for g in self.policy.gpu_affinity) + return env + def set_up_new_pipeline_dirs(self, new_pipeline_name): base_output = os.path.join( - self.base_path, "af_pipeline_outputs_multi", new_pipeline_name + self.output_base_path, "af_pipeline_outputs_multi", new_pipeline_name ) - input_dir = os.path.join(self.base_path, f"{new_pipeline_name}_in") + input_dir = os.path.join(self.input_base_path, f"{new_pipeline_name}_in") - if os.path.isdir(base_output): - return # already exists, nothing to do + # No early-return guard: max_passes may have changed since the directory was + # first created, and exist_ok=True makes every makedirs call idempotent. # all directories to create subdirs = [ @@ -73,7 +110,7 @@ def set_up_new_pipeline_dirs(self, new_pipeline_name): "af/prediction/dimer_models", "af/prediction/logs", "mpnn", - *[f"mpnn/job_{i}" for i in range(1, 6)], + *[f"mpnn/job_{i}" for i in range(1, self.max_passes + 1)], ] paths_to_create = [input_dir, base_output] + [ @@ -86,16 +123,16 @@ def set_up_new_pipeline_dirs(self, new_pipeline_name): def register_pipeline_tasks(self): """Register all pipeline tasks""" - @self.auto_register_task() # MPNN - async def s1(task_description={"gpus_per_rank": 1}): # noqa: B006 - mpnn_script = os.path.join(self.base_path, "mpnn_wrapper.py") + @self.auto_register_task(local_task=True) # MPNN + async def s1(): + mpnn_script = os.path.join(self.scripts_path, "mpnn_wrapper.py") output_dir = os.path.join(self.output_path_mpnn, f"job_{self.passes}") chain = "A" if self.passes == 1 else "B" input_path = self.input_path if self.passes == 1 else self.output_path_af - return ( - f"python3 {mpnn_script} " + cmd = ( + f"python {mpnn_script} " f"-pdb={input_path} " f"-out={output_dir} " f"-mpnn={self.mpnn_path} " @@ -103,6 +140,17 @@ async def s1(task_description={"gpus_per_rank": 1}): # noqa: B006 "-is_monomer=0 " f"-chains={chain}" ) + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + env=self._gpu_env(), + ) + stdout, _ = await proc.communicate() + if stdout: + print(stdout.decode(), end="", flush=True) + if proc.returncode != 0: + raise RuntimeError(f"MPNN failed with exit code {proc.returncode}") @self.auto_register_task(local_task=True) async def s2(): @@ -121,7 +169,7 @@ async def s2(): else: seqs.append([line, score]) - seqs.sort(key=lambda x: x[1]) # Sort by score + seqs.sort(key=lambda x: x[1], reverse=True) # descending: best (least-negative log-prob) first self.iter_seqs[file_name.split(".")[0]] = seqs # fasta - don't use helper script - cannot run x tasks for x structures @@ -143,25 +191,71 @@ async def s3(): return fasta_file_to_return # alphafold, must be run separately for each structure one at a time! - @self.auto_register_task() - async def s4(target_fasta, task_description={"gpus_per_rank": 1}): # noqa: B006 + @self.auto_register_task(local_task=True) + async def s4(target_fasta): cmd = ( - f"/bin/bash {self.base_path}/af2_multimer_reduced.sh " + f"/bin/bash {self.scripts_path}/af2_multimer_reduced.sh " f"{self.output_path}/af/fasta/ " f"{target_fasta}.fa " f"{self.output_path}/af/prediction/dimer_models/ " ) + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + env=self._gpu_env(), + ) + stdout, _ = await proc.communicate() + if stdout: + print(stdout.decode(), end="", flush=True) + if proc.returncode != 0: + raise RuntimeError(f"AlphaFold failed with exit code {proc.returncode}") - return cmd + @self.auto_register_task(local_task=True) + async def s4_post(target_fasta): + import glob + import shutil - @self.auto_register_task() # pLDTT_extract - async def s5(task_description={}): # noqa: B006 - return ( - f"python3 {self.base_path}/plddt_extract_pipeline.py " - f"--path={self.base_path} " + models_path = os.path.join( + self.output_path, "af", "prediction", "dimer_models", target_fasta + ) + best_model_pdb = os.path.join( + self.output_path, "af", "prediction", "best_models", f"{target_fasta}.pdb" + ) + best_ptm_json = os.path.join( + self.output_path, "af", "prediction", "best_ptm", f"{target_fasta}.json" + ) + mpnn_pdb = os.path.join( + self.output_path, "mpnn", f"job_{self.passes}", f"{target_fasta}.pdb" + ) + + ranked0 = glob.glob(os.path.join(models_path, "*ranked_0*.pdb")) + ranking_debug = glob.glob(os.path.join(models_path, "*ranking_debug*.json")) + + if ranked0: + shutil.copy(ranked0[0], best_model_pdb) + shutil.copy(ranked0[0], mpnn_pdb) + if ranking_debug: + shutil.copy(ranking_debug[0], best_ptm_json) + + @self.auto_register_task(local_task=True) # pLDTT_extract + async def s5(): + cmd = ( + f"python3 {self.scripts_path}/plddt_extract_pipeline.py " + f"--path={self.output_base_path} " f"--iter={self.passes} " f"--out={self.name}" ) + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + stdout, _ = await proc.communicate() + if stdout: + print(stdout.decode(), end="", flush=True) + if proc.returncode != 0: + raise RuntimeError(f"pLDDT extraction failed with exit code {proc.returncode}") async def get_scores_map(self): """Return current and previous scores""" @@ -193,7 +287,7 @@ async def run(self): else: self.logger.pipeline_log("Submitting MPNN task") - await self.s1(task_description={"pre_exec": TASK_PRE_EXEC}) + await self.s1() self.logger.pipeline_log("MPNN task finished") self.logger.pipeline_log("Submitting sequence ranking task") @@ -207,66 +301,29 @@ async def run(self): alphafold_tasks = [] for target_fasta in fasta_files: - models_path = os.path.join( - self.output_path, "af", "prediction", "dimer_models", target_fasta - ) - - best_model_pdb = os.path.join( - self.output_path, - "af", - "prediction", - "best_models", - f"{target_fasta}.pdb", - ) - best_ptm_json = os.path.join( - self.output_path, - "af", - "prediction", - "best_ptm", - f"{target_fasta}.json", - ) - mpnn_pdb = os.path.join( - self.output_path, - "mpnn", - f"job_{self.passes}", - f"{target_fasta}.pdb", - ) - - s4_description = { - "pre_exec": TASK_PRE_EXEC, - "post_exec": [ - f"cp {models_path}/*ranked_0*.pdb {best_model_pdb}", - f"cp {models_path}/*ranking_debug*.json {best_ptm_json}", - f"cp {models_path}/*ranked_0*.pdb {mpnn_pdb}", - ], - } - # launch coroutine without awaiting yet - alphafold_tasks.append( - self.s4(target_fasta=target_fasta, task_description=s4_description) - ) + alphafold_tasks.append(self.s4(target_fasta=target_fasta)) self.logger.pipeline_log( f"Submitting {len(alphafold_tasks)} Alphafold tasks asynchronously" ) - await asyncio.gather(*alphafold_tasks, return_exceptions=True) + results = await asyncio.gather(*alphafold_tasks, return_exceptions=True) + failed = [(i, r) for i, r in enumerate(results) if isinstance(r, Exception)] + for i, r in failed: + self.logger.pipeline_log(f"AlphaFold task {i} FAILED: {r}") + if failed: + raise RuntimeError( + f"{len(failed)}/{len(alphafold_tasks)} AlphaFold task(s) failed — aborting pass {self.passes}" + ) self.logger.pipeline_log(f"{len(alphafold_tasks)} Alphafold tasks finished") - self.logger.pipeline_log("Submitting pLDTT extraction task") + self.logger.pipeline_log("Copying AlphaFold best models") + for target_fasta in fasta_files: + await self.s4_post(target_fasta=target_fasta) + self.logger.pipeline_log("AlphaFold best models copied") - staged_file = f"af_stats_{self.name}_pass_{self.passes}.csv" - - await self.s5( - task_description={ - "pre_exec": TASK_PRE_EXEC, - "output_staging": [ - { - "source": f"task:///{staged_file}", - "target": f"client:///{staged_file}", - } - ], - } - ) + self.logger.pipeline_log("Submitting pLDTT extraction task") + await self.s5() self.logger.pipeline_log("pLDTT extract finished") await self.run_adaptive_step(wait=True) From d8c1b4b56709c168f8aded786b5a06c12e543843 Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Sat, 29 Aug 2026 07:49:42 -0500 Subject: [PATCH 02/12] Port small_molecule_binding to Delta HPC; fix manager bug; add code reviews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pipeline changes (examples/small_molecule_binding/): - Rewrite all tasks from capture_stdio to local_task=True using asyncio.create_subprocess_shell; redirect stdout+stderr to per-task .log files (only surface on failure) - Add _gpu_env() helper to propagate Dragon Policy gpu_affinity via CUDA_VISIBLE_DEVICES - Add CIF.GZ→PDB gemmi conversion before MPNN (LigandMPNN only reads PDB format) - Make scripts_path overridable via kwargs - Add mpnn_run.py: numpy deprecated alias shim for LigandMPNN's bundled openfold af2.sh: - Detect colabfold_batch from active venv or pixi env; error if not found - Use COLABFOLD_CACHE_DIR env var for model weights; fallback to ~/.cache/colabfold - Pass --data flag (not --data-dir) and --num-models 1 for integration runs rfd3.sh: - Add --writable-tmpfs --bind /scratch:/scratch to apptainer exec (required on Delta) - Set PYTHONNOUSERSITE=1 to prevent host .local packages contaminating container impress_manager.py (bug fix): - kill_parent path appended bare pipeline instead of (pipeline, future) tuple → ValueError on unpack in cleanup loop; fixed to append (pipeline, pipeline_future) - Surface pipeline exceptions via future.exception() and log via pipeline_failed() logger.py: - Add pipeline_failed() method for error-level pipeline failure reporting Code reviews: - src/CODE_REVIEW.md: bugs and quality issues in impress src/ - examples/small_molecule_binding/CODE_REVIEW.md: pipeline + scripts review - examples/protien_binding_usecase/CODE_REVIEW.md: protein binding example review Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DbNxkFCwkCEEkHnN8xGH7Q --- .../protien_binding_usecase/CODE_REVIEW.md | 209 ++++++++++++++++++ .../small_molecule_binding/CODE_REVIEW.md | 206 +++++++++++++++++ .../run_small_molecule_binding.py | 4 +- .../small_molecule_binding/scripts/af2.sh | 36 ++- .../scripts/fastrelax.py | 2 +- .../scripts/fastrelax.sh | 2 +- .../scripts/filter_shape.py | 2 +- .../scripts/filter_shape.sh | 2 +- .../small_molecule_binding/scripts/mpnn.sh | 6 +- .../scripts/mpnn_run.py | 25 +++ .../small_molecule_binding/scripts/packmin.py | 2 +- .../small_molecule_binding/scripts/packmin.sh | 2 +- .../small_molecule_binding/scripts/rfd3.sh | 7 +- .../small_molecule_binding.py | 123 +++++++++-- src/CODE_REVIEW.md | 162 ++++++++++++++ src/impress/impress_manager.py | 19 +- src/impress/utils/logger.py | 5 + 17 files changed, 773 insertions(+), 41 deletions(-) create mode 100644 examples/protien_binding_usecase/CODE_REVIEW.md create mode 100644 examples/small_molecule_binding/CODE_REVIEW.md create mode 100644 examples/small_molecule_binding/scripts/mpnn_run.py create mode 100644 src/CODE_REVIEW.md diff --git a/examples/protien_binding_usecase/CODE_REVIEW.md b/examples/protien_binding_usecase/CODE_REVIEW.md new file mode 100644 index 0000000..d79c1ee --- /dev/null +++ b/examples/protien_binding_usecase/CODE_REVIEW.md @@ -0,0 +1,209 @@ +# Code Review — `examples/protien_binding_usecase/` + +**Date:** 2026-08-29 +**Scope:** `run_protein_binding.py`, `mpnn_wrapper.py`, +`plddt_extract_pipeline.py`, `af2_multimer_reduced.sh`, `delta_gpu_run.sh` + +--- + +## Bugs + +### `mpnn_wrapper.py:1` — Python file with `#!/bin/sh` shebang + +Line 1 is `#!/bin/sh`, making the OS attempt to execute a Python file as a POSIX +shell script. Any direct execution of `./mpnn_wrapper.py` produces a parse error at +the first Python statement. The file should have `#!/usr/bin/env python3` or no +shebang at all. + +--- + +### `mpnn_wrapper.py:18` — `--temp` argument parses temperature as `int` instead of `float` + +```python +parser.add_argument("-temp", "--temp", ..., type=int, default=0.1) +``` + +`type=int` truncates `0.1` to `0`. Every invocation that relies on the default (or +passes a fractional temperature) gets temperature `0`, which collapses the sampling +distribution to greedy argmax. `type=float` is required. + +--- + +### `plddt_extract_pipeline.py:122` — division by zero if `counter2` is 0 + +```python +avg_pae = running_sum / counter2 +``` + +`counter2` counts PAE matrix cells where exactly one of `row_index` / `col_index` +falls in `target_range`. If `target_range` is empty (protein shorter than 10 residues) +or the PAE matrix is empty, `counter2` stays 0 and this line raises `ZeroDivisionError`. + +--- + +## Potential Issues + +### `run_protein_binding.py:50` — adaptive function reads CSV from current working directory + +```python +file_name = f'af_stats_{pipeline.name}_pass_{pipeline.passes}.csv' +with open(file_name) as fd: +``` + +This path is relative to wherever the process was started (typically the script +directory). If the working directory is wrong or if AF2 fails and the file was never +written, this raises `FileNotFoundError` and crashes the adaptive function, leaving +the pipeline in an undefined state. Should use an absolute path based on `pipeline.base_path` +and wrap with `try/except`. + +--- + +### `run_protein_binding.py:15` — `adaptive_criteria` declared `async` with no awaits + +```python +async def adaptive_criteria(current_score: float, previous_score: float) -> bool: + return current_score > previous_score +``` + +This function does no I/O or async work. Declaring it `async` adds unnecessary +overhead at every call site. Make it a plain `def`. + +--- + +### `plddt_extract_pipeline.py` — incompatible with current IMPRESS output structure + +`on_replica_done` in the current campaign (`sm_binding_workflow.py`) reads +`binder_scores_*.json` directly from the alphafold output directory. This script reads +from `af_pipeline_outputs_multi/{name}/af/prediction/best_models` — a directory tree +that the current pipeline does not create. This script is effectively stale and will +produce empty / zero results if run against current outputs. + +--- + +### `delta_gpu_run.sh:5,44` — SBATCH requests 4 tasks but runs single-node Dragon + +``` +#SBATCH --tasks-per-node=4 +... +dragon -s run_protein_binding.py +``` + +`dragon -s` is single-node mode. Requesting 4 tasks-per-node wastes resources and +may confuse the scheduler. For true multi-node, change to `dragon -m`; for +single-node, change `--tasks-per-node=1`. + +--- + +### `delta_gpu_run.sh:21` — unguarded `LD_LIBRARY_PATH` produces trailing colon + +```bash +export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH} +``` + +If `LD_LIBRARY_PATH` is unset, this expands to a trailing `:`, which adds the current +directory to the dynamic linker search path — a security risk. Should be +`${LD_LIBRARY_PATH:-}`. + +--- + +## Code Quality + +### `mpnn_wrapper.py` — massive duplicated subprocess.call blocks + +The file's 140 lines handle 12+ scenario combinations (monomer/multimer × +fixed/unfixed × tied/homomer/interface) through deeply nested `if/else` with nearly +identical `subprocess.call` lists. The only differences are which `--*_jsonl` flags +are appended. Refactor to build the argument list incrementally and call +`subprocess.call` once. + +--- + +### `mpnn_wrapper.py` — not used by the current pipeline + +The active pipeline calls `mpnn.sh` → `mpnn_run.py` → LigandMPNN's `run.py`. +`mpnn_wrapper.py` calls `protein_mpnn_run.py` (ProteinMPNN, not LigandMPNN) via a +completely different argument schema. This file is dead code relative to the current +pipeline and should be either removed, labelled as a standalone utility, or replaced +with a properly integrated version. + +--- + +### `mpnn_wrapper.py:30-40` — `chains == None` instead of `chains is None` + +```python +if chains == None: + chains = 'A' +``` + +PEP 8 and Python convention require `if chains is None:`. The `==` form works for +`None` but is non-idiomatic and triggers linter warnings. + +--- + +### `run_protein_binding.py:75` — typo in log message + +```python +pipeline.logger.pipeline_log(f'Adaptive descision: {decision}') +``` + +"descision" → "decision". + +--- + +### `run_protein_binding.py:39` — `eval` for venv activation + +```bash +eval "$IMPRESS_PRE_EXEC" +``` + +(in `delta_gpu_run.sh` line 39) Running an env-supplied string through `eval` allows +arbitrary code execution if `IMPRESS_PRE_EXEC` is set adversarially or incorrectly. +Replace with a direct `source` of the known venv path. + +--- + +### `plddt_extract_pipeline.py:74` — unclosed file handle + +```python +data = json.load(open(data_path)) +``` + +The file handle is never closed. Should use `with open(data_path) as f: json.load(f)`. + +--- + +### `plddt_extract_pipeline.py` — extensive commented-out code + +Lines 56–60 (biopandas block), 83–92 (`df_json`), 95–100 (print block), and 103–107 +(debug prints) are commented-out code that was never removed. These should be deleted +to improve readability. + +--- + +### `plddt_extract_pipeline.py` — manual index tracking instead of `enumerate` + +Lines 111–122 use manual `row_index`/`col_index` variables incremented in loop bodies +to track which matrix cell is being accessed. Using `enumerate` would be clearer and +less error-prone. + +--- + +### `af2_multimer_reduced.sh:13-14` — `/tmp/work` and `/tmp/upper` created but never used + +```bash +WORK=/tmp/work +UPPER=/tmp/upper +mkdir -p $WORK $UPPER +``` + +These directories are never referenced in the apptainer call. Leftover from a prior +overlay filesystem approach. Remove. + +--- + +### Directory name typo: `protien_binding_usecase` + +The directory is named `protien_binding_usecase` (protein misspelled). The companion +`IMPRESS/src/impress/pipelines/protein_binding.py` is spelled correctly. For new +branches, consider renaming to `protein_binding_usecase` to avoid confusion, although +this changes an established path. diff --git a/examples/small_molecule_binding/CODE_REVIEW.md b/examples/small_molecule_binding/CODE_REVIEW.md new file mode 100644 index 0000000..127fa00 --- /dev/null +++ b/examples/small_molecule_binding/CODE_REVIEW.md @@ -0,0 +1,206 @@ +# Code Review — `examples/small_molecule_binding/` + +**Date:** 2026-08-29 +**Scope:** `small_molecule_binding.py`, `run_small_molecule_binding.py`, +`run_test_small_molecule_binding.py`, `mock.py`, `scripts/` + +--- + +## Bugs + +### `filter_shape.py:38` — undefined `sfxn` in RosettaScripts XML + +The XML passed to `XmlObjects.create_from_string` references `score_fxn="sfxn"` in the +`ScoreTermValueBased` residue selector, but the `` block only defines +`sfxn_clean`. Rosetta raises an error at parse time and analysis fails for every PDB. + +**Fix:** change the selector to `score_fxn="sfxn_clean"` to match the defined score +function, or add a `sfxn` score function definition. + +--- + +### `rfd3.sh:5-6` — comment swaps `$4`/`$5` argument order + +The header comment says `$4=scaffold_arg $5=diffusion_batch_size`, but the actual +positional assignments and the call site in `small_molecule_binding.py` are +`$4=diffusion_batch_size $5=scaffold_arg`. The code is correct; the comment is +misleading and will cause confusion when the script is modified. + +--- + +### `packmin.py:1-22` — old-style `from rosetta.*` imports + +Lines 10–22 import from `rosetta.core.*`, `rosetta.protocols.*` etc. (old pre-3.8 +binding namespace). Modern PyRosetta exposes these only under `pyrosetta.rosetta.*`. +On Delta (where a current PyRosetta is installed), all of these imports will raise +`ModuleNotFoundError` at startup. + +**Fix:** replace all `from rosetta.X import Y` with `from pyrosetta.rosetta.X import Y` +(or remove unused imports — most of the imported symbols are never referenced in the +actual code body). + +--- + +## Potential Issues + +### `small_molecule_binding.py:149,152,153` — Anvil-specific hard-coded default paths + +```python +self.mpnn_dir = kwargs.get("mpnn_dir", "/anvil/projects/x-nairr240405/mason/LigandMPNN") +self.foundry_sif_path = kwargs.get("foundry_sif_path", "/anvil/projects/x-nairr240405/mason/foundry.sif") +self.colabfold_path = kwargs.get("colabfold_path", "/anvil/projects/x-nairr240405/mason/localcolabfold") +``` + +These defaults silently resolve to non-existent paths on any system other than Anvil +and the tools fail at runtime with no clear error that the path is wrong. Callers on +Delta must remember to override all three via `kwargs` or env vars. + +**Recommended fix:** default to `None` and raise a clear `ValueError` in `__init__` if +the path is not supplied. Or at minimum document the required overrides prominently. + +--- + +### `small_molecule_binding.py:367,449,489,605` — analysis tasks infer taskdir from `self.taskcount` + +Every analysis task (e.g. `analysis_sequence`) computes its input directory as +`{base_path}/{name}/{self.taskcount}_mpnn/out`. This works only because the +corresponding computation task (`mpnn`) incremented `taskcount` immediately before. +If any other counted task is inserted between computation and analysis, the path +silently points to the wrong directory and the task reads stale or absent files. + +--- + +### `run_small_molecule_binding.py:20` — rhapsody DEBUG logging enabled unconditionally + +```python +rhapsody.enable_logging(level=logging.DEBUG) +``` + +DEBUG-level rhapsody logs every Dragon message exchange. On a 8-pipeline run this +generates thousands of lines per second and buries application output. + +--- + +### `af2.sh:40` — `--num-models 1` is an integration-only flag + +Running with a single model is fast but produces lower-quality predictions. This was +set during integration testing and was not reverted for production. Should either be +removed or made configurable via a script argument. + +--- + +### `mock.py` — taskdir path is missing the pipeline name component + +Real task dirs: `{base_path}/{pipeline_name}/{count}_taskname` +Mock task dirs: `{base_path}/{count}_taskname` + +The mock directory structure is inconsistent with the real one. This means mock tests +do not exercise the same path logic as real runs. Any test that checks or mocks the +directory layout will diverge silently from production behavior. + +--- + +## Code Quality + +### `small_molecule_binding.py:691` — commented-out `fixed_residues_file` argument + +```python +# fixed_residues_file=f"{self.pipeline_inputs}/fixed_residues.txt" ) +``` + +The `fixed_residues_file` parameter is accepted by `mpnn()` but never passed in the +refine cycle. Either wire it in or remove the dead parameter and the comment. + +--- + +### `run_small_molecule_binding.py:3` — unused imports + +```python +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor +``` + +Neither is used anywhere in the file. Remove them. + +--- + +### `run_small_molecule_binding.py:155` — hardcoded pipeline count (8 pipelines) + +```python +for i in range(1, 9) +``` + +The number of pipelines is baked in as a magic literal. Should be a named constant or +a command-line argument. + +--- + +### `filter_shape.py:6-9` — bare `sys.argv` instead of argparse + +Arguments are taken as `sys.argv[1..4]` with no validation. Wrong argument count +causes an unhelpful `IndexError`. Both `fastrelax.py` and `packmin.py` use `argparse` +properly — `filter_shape.py` should too. + +--- + +### `filter_shape.py:18-21`, `118-129` — explicit `.close()` inside `with` blocks + +```python +with open(gen_output_file, 'w') as genout: + genout.write(...) + genout.close() # redundant +``` + +Calling `.close()` inside a `with` block is redundant and confusing — the context +manager closes the file on exit. Remove all explicit `.close()` calls inside `with` +blocks. + +--- + +### `filter_energy.py` — always prints to stdout + +Line 45: `print(f"Processed: {pdb_file}, ...")` prints to stdout for every file. When +the subprocess runs with stdout redirected to a log file this ends up in the log, but +if stderr is inspected separately the output is silent. Behaviour is inconsistent with +all other scripts that write only on failure. + +--- + +### `fastrelax.sh:12` — unquoted `$0` in `dirname` + +```bash +SCRIPT_DIR="$(dirname $0)" # wrong +``` + +Should be `$(dirname "$0")` to handle paths with spaces. `filter_shape.sh` and +`packmin.sh` already use the quoted form — this is the one outlier. + +--- + +### `packmin.py` — large commented-out code blocks + +Lines 96–116 contain multi-line commented-out tutorial fragments, original import +notes, and unreachable code. Clean these up before publication. + +--- + +### `mpnn_wrapper.sh` — legacy script, superseded by `mpnn.sh` + +`mpnn_wrapper.sh` calls `run.py` directly (bypassing the numpy alias fix in +`mpnn_run.py`), hardcodes `echo "A16" > fixed_residues.txt`, and invokes +`protein_mpnn_run.py` (ProteinMPNN) rather than LigandMPNN's `run.py`. The active +pipeline uses `mpnn.sh` → `mpnn_run.py`. This script is dead code and should be +removed or explicitly marked as deprecated. + +--- + +### `af2.sh:31,36` — diagnostic echo lines remain + +Two `echo` lines print to the log file: + +```bash +echo "[af2.sh] using colabfold_batch: $colabfold_bin" +echo "[af2.sh] data_dir: $data_dir" +``` + +Acceptable while debugging, but should be removed or made conditional on a `VERBOSE` +flag before pushing to the shared branch. diff --git a/examples/small_molecule_binding/run_small_molecule_binding.py b/examples/small_molecule_binding/run_small_molecule_binding.py index 6ec5650..3735769 100644 --- a/examples/small_molecule_binding/run_small_molecule_binding.py +++ b/examples/small_molecule_binding/run_small_molecule_binding.py @@ -4,7 +4,7 @@ from typing import List from radical.asyncflow import LocalExecutionBackend -from rhapsody.backends import DragonExecutionBackendV3 +from rhapsody.backends import DragonExecutionBackend from impress import ImpressManager, PipelineSetup from small_molecule_binding import ( @@ -135,7 +135,7 @@ def _prior(ttype): async def impress_smallmol_bind() -> None: """Execute the small-molecule binding pipeline.""" #backend = await LocalExecutionBackend(ProcessPoolExecutor()) - backend = await DragonExecutionBackendV3() + backend = await DragonExecutionBackend() manager: ImpressManager = ImpressManager(execution_backend=backend) pipeline_setups: List[PipelineSetup] = [ diff --git a/examples/small_molecule_binding/scripts/af2.sh b/examples/small_molecule_binding/scripts/af2.sh index ede53a8..59c4be3 100755 --- a/examples/small_molecule_binding/scripts/af2.sh +++ b/examples/small_molecule_binding/scripts/af2.sh @@ -1,20 +1,44 @@ #!/bin/bash set -euo pipefail -# AlphaFold2 structure prediction via LocalColabFold (pixi) +# AlphaFold2 structure prediction via LocalColabFold # Args: $1=colabfold_path $2=short_fasta $3=output_dir +# +# colabfold_path: root of the localcolabfold repo (contains pyproject.toml). +# colabfold_batch is resolved from .pixi/envs/default/bin/ under that root, +# bypassing pixi (not available on Delta). colabfold_path="$1" short_fasta="$2" output_dir="$3" -module load gcc/11.2.0 -module load cuda -source /anvil/projects/x-nairr240405/mason/IMPRESS/.venv/bin/activate +# Prefer the venv's colabfold_batch (installed in IMPRESS venv) over the +# hooten1 pixi env, whose Python files are not world-readable on Delta. +VENV_BIN="$(dirname "$(command -v python 2>/dev/null)")" +colabfold_bin="" +for candidate in \ + "${VENV_BIN}/colabfold_batch" \ + "${colabfold_path}/.pixi/envs/default/bin/colabfold_batch"; do + if [ -x "$candidate" ]; then + colabfold_bin="$candidate" + break + fi +done +if [ -z "$colabfold_bin" ]; then + echo "ERROR: colabfold_batch not found in venv or at $colabfold_path" >&2 + exit 1 +fi +echo "[af2.sh] using colabfold_batch: $colabfold_bin" -pixi run --manifest-path "$colabfold_path" \ - colabfold_batch \ +# Use scratch for the model weights cache to avoid home quota exhaustion. +# COLABFOLD_CACHE_DIR must be set (delta_sbatch.sh exports it). +data_dir="${COLABFOLD_CACHE_DIR:-${HOME}/.cache/colabfold}" +echo "[af2.sh] data_dir: $data_dir" + +"$colabfold_bin" \ --model-type alphafold2 \ + --num-models 1 \ + --data "$data_dir" \ --rank auto \ --random-seed 999 \ --save-all \ diff --git a/examples/small_molecule_binding/scripts/fastrelax.py b/examples/small_molecule_binding/scripts/fastrelax.py index b899106..cfd78be 100755 --- a/examples/small_molecule_binding/scripts/fastrelax.py +++ b/examples/small_molecule_binding/scripts/fastrelax.py @@ -100,7 +100,7 @@ def main(args): if __name__ == '__main__': args = parse_args() - opts = '-ex1 -ex2 -use_input_sc -flip_HNQ -no_optH false -out:level 500' + opts = '-ex1 -ex2 -use_input_sc -flip_HNQ -no_optH false -mute all' if args.constraints: opts += ' -enzdes::cstfile {}'.format(args.constraints) opts += ' -run:preserve_header' diff --git a/examples/small_molecule_binding/scripts/fastrelax.sh b/examples/small_molecule_binding/scripts/fastrelax.sh index 57a3987..5d845f3 100755 --- a/examples/small_molecule_binding/scripts/fastrelax.sh +++ b/examples/small_molecule_binding/scripts/fastrelax.sh @@ -10,7 +10,7 @@ output_dir="$3" SCRIPT_DIR="$(dirname $0)" -source /anvil/projects/x-nairr240405/mason/IMPRESS/.venv/bin/activate +source "${ENV_DIR:-/u/${USER}/ve/impress}/bin/activate" python "$SCRIPT_DIR/fastrelax.py" \ "$pdb_path" \ diff --git a/examples/small_molecule_binding/scripts/filter_shape.py b/examples/small_molecule_binding/scripts/filter_shape.py index 87278a5..fcd8d92 100644 --- a/examples/small_molecule_binding/scripts/filter_shape.py +++ b/examples/small_molecule_binding/scripts/filter_shape.py @@ -8,7 +8,7 @@ ligand_name = sys.argv[3] # 'ALR' gen_output_file = sys.argv[4] # 'interface_values.txt' -pyrosetta.init(f"-ignore_unrecognized_res -ignore_zero_occupancy --extra_res_fa {ligand_name}.params -corrections::beta_nov16 true") +pyrosetta.init(f"-ignore_unrecognized_res -ignore_zero_occupancy --extra_res_fa {ligand_name}.params -corrections::beta_nov16 true -mute all") # Define directories and files #pdb_directory = '/WWW/PDB_Files' diff --git a/examples/small_molecule_binding/scripts/filter_shape.sh b/examples/small_molecule_binding/scripts/filter_shape.sh index 2208834..2bd1844 100755 --- a/examples/small_molecule_binding/scripts/filter_shape.sh +++ b/examples/small_molecule_binding/scripts/filter_shape.sh @@ -11,7 +11,7 @@ interface_values_output="$4" SCRIPT_DIR="$(dirname "$0")" -source /anvil/projects/x-nairr240405/mason/IMPRESS/.venv/bin/activate +source "${ENV_DIR:-/u/${USER}/ve/impress}/bin/activate" python "$SCRIPT_DIR/filter_shape.py" \ "$pdb_directory" \ diff --git a/examples/small_molecule_binding/scripts/mpnn.sh b/examples/small_molecule_binding/scripts/mpnn.sh index c5aaf1d..29378eb 100755 --- a/examples/small_molecule_binding/scripts/mpnn.sh +++ b/examples/small_molecule_binding/scripts/mpnn.sh @@ -11,9 +11,11 @@ n_batches="$4" batch_size="$5" fixed_residues="${6:-}" -source /anvil/projects/x-nairr240405/mason/LigandMPNN/.venv/bin/activate +SCRIPT_DIR="$(dirname "$0")" -python "$mpnn_dir/run.py" \ +# mpnn_run.py restores numpy deprecated aliases (np.int/np.bool/np.object) +# removed in NumPy 1.24+ that LigandMPNN's bundled openfold still uses. +python "$SCRIPT_DIR/mpnn_run.py" "$mpnn_dir" \ --model_type "ligand_mpnn" \ --checkpoint_path_sc "$mpnn_dir/model_params/ligandmpnn_sc_v_32_002_16.pt" \ --checkpoint_ligand_mpnn "$mpnn_dir/model_params/ligandmpnn_v_32_010_25.pt" \ diff --git a/examples/small_molecule_binding/scripts/mpnn_run.py b/examples/small_molecule_binding/scripts/mpnn_run.py new file mode 100644 index 0000000..4c7cfd2 --- /dev/null +++ b/examples/small_molecule_binding/scripts/mpnn_run.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +""" +Wrapper for LigandMPNN run.py that restores deprecated numpy aliases removed +in NumPy 1.24+. LigandMPNN's bundled openfold uses np.int, np.object, np.bool. + +Usage (from mpnn.sh): + python mpnn_run.py [run.py args...] +The mpnn_dir is stripped from sys.argv before run.py sees it. +""" +import sys +import os + +import numpy as np +for _alias, _builtin in [('int', int), ('float', float), ('bool', bool), + ('complex', complex), ('object', object), ('str', str)]: + if not hasattr(np, _alias): + setattr(np, _alias, _builtin) + +mpnn_dir = sys.argv[1] +sys.argv = [os.path.join(mpnn_dir, 'run.py')] + sys.argv[2:] +sys.path.insert(0, mpnn_dir) +os.chdir(mpnn_dir) + +import runpy +runpy.run_path(os.path.join(mpnn_dir, 'run.py'), run_name='__main__') diff --git a/examples/small_molecule_binding/scripts/packmin.py b/examples/small_molecule_binding/scripts/packmin.py index 8dcbf97..04fefc8 100644 --- a/examples/small_molecule_binding/scripts/packmin.py +++ b/examples/small_molecule_binding/scripts/packmin.py @@ -120,7 +120,7 @@ def main(args): if __name__ == '__main__': args = parse_args() - opts = '-ex1 -ex2 -use_input_sc -flip_HNQ -no_optH false' + opts = '-ex1 -ex2 -use_input_sc -flip_HNQ -no_optH false -mute all' if args.constraints: opts += ' -enzdes::cstfile {}'.format(args.constraints) opts += ' -run:preserve_header' diff --git a/examples/small_molecule_binding/scripts/packmin.sh b/examples/small_molecule_binding/scripts/packmin.sh index 5ce3be7..cf2d058 100755 --- a/examples/small_molecule_binding/scripts/packmin.sh +++ b/examples/small_molecule_binding/scripts/packmin.sh @@ -10,7 +10,7 @@ output_dir="$3" SCRIPT_DIR="$(dirname "$0")" -source /anvil/projects/x-nairr240405/mason/IMPRESS/.venv/bin/activate +source "${ENV_DIR:-/u/${USER}/ve/impress}/bin/activate" python "$SCRIPT_DIR/packmin.py" \ "$pdb_path" \ diff --git a/examples/small_molecule_binding/scripts/rfd3.sh b/examples/small_molecule_binding/scripts/rfd3.sh index 33b27e3..b75bb26 100755 --- a/examples/small_molecule_binding/scripts/rfd3.sh +++ b/examples/small_molecule_binding/scripts/rfd3.sh @@ -16,7 +16,12 @@ else scaffold_arg="" fi -apptainer exec --nv "$foundry_sif_path" rfd3 design \ +# Prevent host ~/.local Python packages from contaminating the container +# (apptainer mounts $HOME by default; PYTHONNOUSERSITE must be SET, not unset). +unset PYTHONPATH PYTHONUSERBASE PYTHONDONTWRITEBYTECODE +export PYTHONNOUSERSITE=1 + +apptainer exec --nv --writable-tmpfs --bind /scratch:/scratch "$foundry_sif_path" rfd3 design \ out_dir="$output_dir" \ inputs="$inputs" \ skip_existing=False \ diff --git a/examples/small_molecule_binding/small_molecule_binding.py b/examples/small_molecule_binding/small_molecule_binding.py index d4264e1..62eb088 100644 --- a/examples/small_molecule_binding/small_molecule_binding.py +++ b/examples/small_molecule_binding/small_molecule_binding.py @@ -1,6 +1,7 @@ import asyncio import copy +import gzip import json import os import pathlib @@ -140,8 +141,10 @@ def __init__(self, name, flow, configs=None, **kwargs): super().__init__(name, flow, **configs, **kwargs) # Paths - self.base_path = kwargs.get("base_path", os.getcwd()) - self.scripts_path = os.path.join(self.base_path, "scripts") + self.base_path = kwargs.get("base_path", os.getcwd()) + self.scripts_path = kwargs.get( + "scripts_path", os.path.join(self.base_path, "scripts") + ) self.pipeline_inputs = os.path.join(self.base_path, f"{self.name}_in") self.mpnn_dir = kwargs.get("mpnn_dir", f"/anvil/projects/x-nairr240405/mason/LigandMPNN") @@ -162,6 +165,7 @@ def __init__(self, name, flow, configs=None, **kwargs): self.interface_min_sc = kwargs.get("interface_min_sc", 0.5) self.fold_min_plddt = kwargs.get("fold_min_plddt", 70.0) self.max_tasks = kwargs.get("max_tasks", 300) + self.policy = kwargs.get("policy", None) # Output paths (legacy) self.output_path = os.path.join(self.base_path, "myoutputs", self.name) @@ -177,6 +181,15 @@ def __init__(self, name, flow, configs=None, **kwargs): self.next_step = STEP_RFD3 self._current_cycle_i = 0 # set by run() before each mpnn call + # ── GPU env helper ───────────────────────────────────────────────────── + + def _gpu_env(self): + """Return subprocess env with CUDA_VISIBLE_DEVICES set from Dragon Policy gpu_affinity.""" + env = {**os.environ} + if self.policy and self.policy.gpu_affinity: + env["CUDA_VISIBLE_DEVICES"] = ",".join(str(g) for g in self.policy.gpu_affinity) + return env + # ── Task registration ────────────────────────────────────────────────── def register_pipeline_tasks(self): @@ -196,8 +209,8 @@ def _register_mock_tasks(self): def _register_real_tasks(self): """Register real HPC tasks that return shell command strings.""" - @self.auto_register_task(capture_stdio=True) - async def rfd3(task_description={"gpus_per_rank": 1}): + @self.auto_register_task(local_task=True) + async def rfd3(): self.taskcount += 1 taskname = "rfd3" self.previous_task = taskname @@ -211,7 +224,7 @@ async def rfd3(task_description={"gpus_per_rank": 1}): input_pdb = self.state.get('rfd3_input_pdb') scaffold_arg = f"scaffoldguided.target_pdb={input_pdb}" if input_pdb else "" - return ( + cmd = ( f"bash {self.scripts_path}/rfd3.sh" f" {self.foundry_sif_path}" f" {output_dir}" @@ -219,6 +232,17 @@ async def rfd3(task_description={"gpus_per_rank": 1}): f" {self.diffusion_batch_size}" f" {scaffold_arg}" ) + log_file = f"{taskdir}/rfd3.log" + with open(log_file, "wb") as _lf: + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=_lf, + stderr=asyncio.subprocess.STDOUT, + env=self._gpu_env(), + ) + await proc.wait() + if proc.returncode != 0: + raise RuntimeError(f"rfd3 failed with exit code {proc.returncode}\nSee {log_file}") @self.auto_register_task(local_task=True) async def analysis_backbone(): @@ -273,7 +297,7 @@ async def analysis_backbone(): ETYPE_BACKBONE, best['ss'], self.state.get('rfd3_input_pdb'), backbone_path, )) - @self.auto_register_task(capture_stdio=True) + @self.auto_register_task(local_task=True) async def mpnn( fixed_residues_file: str | None = None): self.taskcount += 1 @@ -300,13 +324,24 @@ async def mpnn( shutil.copy(pdb_path_orig, short_pdb) pdb_path = short_pdb + # LigandMPNN (ProDy parsePDB) only reads PDB format; convert CIF.GZ. + if pdb_path.endswith('.cif.gz'): + import gemmi as _gemmi + pdb_for_mpnn = f"{taskdir}/in/binder.pdb" + with gzip.open(pdb_path, 'rb') as _f: + _cif_data = _f.read().decode() + _doc = _gemmi.cif.read_string(_cif_data) + _st = _gemmi.make_structure_from_block(_doc.sole_block()) + _st.write_pdb(pdb_for_mpnn) + pdb_path = pdb_for_mpnn + if fixed_residues_file: with open(fixed_residues_file) as f: fixed_residues = f.read().strip() else: fixed_residues = "" - return ( + cmd = ( f"bash {self.scripts_path}/mpnn.sh" f" {self.mpnn_dir}" f" {pdb_path}" @@ -315,6 +350,17 @@ async def mpnn( f" {batch_size}" f' "{fixed_residues}"' ) + log_file = f"{taskdir}/mpnn.log" + with open(log_file, "wb") as _lf: + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=_lf, + stderr=asyncio.subprocess.STDOUT, + env=self._gpu_env(), + ) + await proc.wait() + if proc.returncode != 0: + raise RuntimeError(f"mpnn failed with exit code {proc.returncode}\nSee {log_file}") @self.auto_register_task(local_task=True) async def analysis_sequence(): @@ -366,7 +412,7 @@ async def analysis_sequence(): self.state.get('best_backbone_path'), self.state.get('last_seq_fasta'), )) - @self.auto_register_task(capture_stdio=True) + @self.auto_register_task(local_task=True) async def packmin(): self.taskcount += 1 taskname = "packmin" @@ -383,12 +429,20 @@ async def packmin(): # Predict output path so the next mpnn can read from it self.state['best_packed_pdb'] = f"{output_dir}/{pdb_stem}_minimized.pdb" - return ( + cmd = ( f"bash {self.scripts_path}/packmin.sh" f" {pdb_path}" f" {lig_path}" f" {output_dir}" ) + log_file = f"{taskdir}/packmin.log" + with open(log_file, "wb") as _lf: + proc = await asyncio.create_subprocess_shell( + cmd, stdout=_lf, stderr=asyncio.subprocess.STDOUT, env=self._gpu_env(), + ) + await proc.wait() + if proc.returncode != 0: + raise RuntimeError(f"packmin failed with exit code {proc.returncode}\nSee {log_file}") @self.auto_register_task(local_task=True) async def analysis_packmin(): @@ -403,7 +457,7 @@ async def analysis_packmin(): self.state['last_analysis_step'] = 'packmin' self.state['last_analysis_metrics'] = {'pass': True, 'total_score': total_score} - @self.auto_register_task(capture_stdio=True) + @self.auto_register_task(local_task=True) async def fastrelax(): self.taskcount += 1 taskname = "fastrelax" @@ -416,13 +470,20 @@ async def fastrelax(): lig_path = f"{self.pipeline_inputs}/{self.ligand_params}" output_dir = f"{taskdir}/out" - return ( + cmd = ( f"bash {self.scripts_path}/fastrelax.sh" f" {pdb_path}" f" {lig_path}" f" {output_dir}" - f" > fastrelax.txt" ) + log_file = f"{taskdir}/fastrelax.log" + with open(log_file, "wb") as _lf: + proc = await asyncio.create_subprocess_shell( + cmd, stdout=_lf, stderr=asyncio.subprocess.STDOUT, env=self._gpu_env(), + ) + await proc.wait() + if proc.returncode != 0: + raise RuntimeError(f"fastrelax failed with exit code {proc.returncode}\nSee {log_file}") @self.auto_register_task(local_task=True) async def analysis_fastrelax(): @@ -452,7 +513,7 @@ async def analysis_fastrelax(): 'rmsd': rmsd, } - @self.auto_register_task(capture_stdio=True) + @self.auto_register_task(local_task=True) async def filter_shape(ligand_name: str = "ALR"): taskname = "filter_shape" taskdir = f"{self.base_path}/{self.name}/{self.taskcount}_{taskname}" @@ -461,13 +522,21 @@ async def filter_shape(ligand_name: str = "ALR"): pdb_directory = f"{self.base_path}/{self.name}/{self.taskcount}_fastrelax/out" - return ( + cmd = ( f"bash {self.scripts_path}/filter_shape.sh" f" {pdb_directory}" f" {taskdir}/out/shape_complementarity_values.txt" f" {self.pipeline_inputs}/{ligand_name}" f" {taskdir}/out/interface_values.txt" ) + log_file = f"{taskdir}/filter_shape.log" + with open(log_file, "wb") as _lf: + proc = await asyncio.create_subprocess_shell( + cmd, stdout=_lf, stderr=asyncio.subprocess.STDOUT, env=self._gpu_env(), + ) + await proc.wait() + if proc.returncode != 0: + raise RuntimeError(f"filter_shape failed with exit code {proc.returncode}\nSee {log_file}") @self.auto_register_task(local_task=True) async def analysis_interface(): @@ -494,8 +563,8 @@ async def analysis_interface(): 'max_sc': max_sc, } - @self.auto_register_task(capture_stdio=True) - async def af2(task_description={"gpus_per_rank": 1}): + @self.auto_register_task(local_task=True) + async def af2(): self.taskcount += 1 taskname = "alphafold" self.previous_task = taskname @@ -516,12 +585,20 @@ async def af2(task_description={"gpus_per_rank": 1}): output_dir = f"{taskdir}/out" - return ( + cmd = ( f"bash {self.scripts_path}/af2.sh" f" {self.colabfold_path}" f" {short_fasta}" f" {output_dir}" ) + log_file = f"{taskdir}/af2.log" + with open(log_file, "wb") as _lf: + proc = await asyncio.create_subprocess_shell( + cmd, stdout=_lf, stderr=asyncio.subprocess.STDOUT, env=self._gpu_env(), + ) + await proc.wait() + if proc.returncode != 0: + raise RuntimeError(f"af2 failed with exit code {proc.returncode}\nSee {log_file}") @self.auto_register_task(local_task=True) async def analysis_fold(): @@ -556,7 +633,7 @@ async def analysis_fold(): 'best_model': best_model, } - @self.auto_register_task(capture_stdio=True) + @self.auto_register_task(local_task=True) async def filter_energy(ligand_name: str = "ALR"): taskname = "filter_energy" taskdir = f"{self.base_path}/{self.name}/{self.taskcount}_{taskname}" @@ -569,7 +646,7 @@ async def filter_energy(ligand_name: str = "ALR"): output_energy_file = f"{outputs_dir}/negative_ligand_energies.txt" common_filenames_file = f"{self.pipeline_inputs}/common_filenames.txt" - return ( + cmd = ( f"bash {self.scripts_path}/filter_energy.sh" f" {pdb_directory}" f" {output_file}" @@ -577,6 +654,14 @@ async def filter_energy(ligand_name: str = "ALR"): f" {common_filenames_file}" f" {ligand_name}" ) + log_file = f"{taskdir}/filter_energy.log" + with open(log_file, "wb") as _lf: + proc = await asyncio.create_subprocess_shell( + cmd, stdout=_lf, stderr=asyncio.subprocess.STDOUT, env=self._gpu_env(), + ) + await proc.wait() + if proc.returncode != 0: + raise RuntimeError(f"filter_energy failed with exit code {proc.returncode}\nSee {log_file}") # ── Score utils ──────────────────────────────────────────────────────── diff --git a/src/CODE_REVIEW.md b/src/CODE_REVIEW.md new file mode 100644 index 0000000..a8291ea --- /dev/null +++ b/src/CODE_REVIEW.md @@ -0,0 +1,162 @@ +# IMPRESS `src/` Code Review + +**Date:** 2026-08-29 +**Scope:** `/scratch/bblj/mgoliyad1/IMPRESS/src/impress/` +**Status:** bugs marked ✅ fixed or ⚠️ open + +--- + +## Bugs + +### ✅ `impress_manager.py:164` — `kill_parent` path crashes on unpack + +When a pipeline sets `kill_parent=True`, the manager appended the bare `pipeline` +object to `completed_pipelines`: + +```python +completed_pipelines.append(pipeline) # was wrong +``` + +The cleanup loop unpacks `for pipeline, future in completed_pipelines:`, so this +raises `ValueError: not enough values to unpack` the moment any pipeline is killed. + +**Fix applied:** changed to `completed_pipelines.append((pipeline, pipeline_future))`. + +--- + +### ⚠️ `protein_binding.py:7–9` — module-level `EnvironmentError` on import + +`MPNN_PATH` is validated at module import time: + +```python +_mpnn = os.environ.get("MPNN_PATH") +if not _mpnn: + raise EnvironmentError("MPNN_PATH is not set ...") +``` + +Any code that does `from impress.pipelines.protein_binding import ...` — even +conditionally — will crash at import if the env var is absent. Should be deferred +to `__init__`. + +--- + +## Potential Issues + +### `impress_manager.py` — `WorkflowEngine` leaks on exception + +`start()` creates `self.flow = await WorkflowEngine.create(...)` but never calls +`self.flow.shutdown()`. The caller is responsible for cleanup, but if `start()` +raises mid-run the caller's post-`await` shutdown line is never reached and the +engine leaks. + +**Recommended fix:** wrap the main loop in `try/finally` inside `start()`, or +document that callers must guard with `try/finally`. + +--- + +### `impress_pipeline.py:99` — `finalize` abstract/async mismatch + +`ImpressBasePipeline` declares: + +```python +@abstractmethod +async def finalize(self): + """Optional: Cleanup or finalization logic""" +``` + +Both `SmallMoleculeBindingPipeline` and `ProteinBindingPipeline` implement it as +a plain `def finalize(self, ...)` (synchronous, with extra args). This means: + +- Calling `await pipeline.finalize()` on a subclass instance would fail because + the method is sync. +- The docstring says "Optional" but `@abstractmethod` makes it mandatory. + +**Recommended fix:** either remove `@abstractmethod` and provide a no-op base +implementation, or declare it consistently as sync across the hierarchy. + +--- + +### `protein_binding.py:303–310` — all AF2 tasks launched concurrently + +```python +results = await asyncio.gather(*alphafold_tasks, return_exceptions=True) +``` + +All structures are folded in parallel. If there are N structures, N AF2 processes +compete for GPU memory simultaneously, likely causing OOM on real runs. AF2 should +be serialised per GPU or gated by a semaphore. + +--- + +## Code Quality + +### `logger.py` — no log level filtering + +`LogLevel` enum is defined but never used to filter output. Every `debug()` call +always prints regardless of any configured level. The `activity_summary` method is +at DEBUG level but fires on every active cycle, producing steady noise. A minimum +configurable log level check should be added to `_write_log` or each log method. + +--- + +### `logger.py` — `error()` ignores `self.output_stream` + +`error()` and `critical()` bypass `self.output_stream` and always write to +`sys.stderr` directly via `_write_log(formatted, to_stderr=True)`. A caller that +sets a custom output stream (e.g. for testing) will silently lose error messages. + +--- + +### `protein_binding.py:185` — hardcoded peptide sequence + +```python +pep_seq = "EGYQDYEPEA" # PDZ-domain peptide +``` + +This is a PDZ-specific constant hardcoded inside `s3()`. It should be a +constructor parameter (e.g. `self.peptide_seq`) so the pipeline is reusable for +other targets. + +--- + +### `protein_binding.py:267–268` — `os.unlink()` without existence check in `finalize()` + +```python +os.unlink(f"{self.output_path_af}/{a}.pdb") +os.unlink(f"{self.output_path}/af/fasta/{a}.fa") +``` + +If an AF2 task failed and the file was never created, `finalize()` raises +`FileNotFoundError`. Should use `pathlib.Path.unlink(missing_ok=True)` or check +existence first. + +--- + +### `protein_binding.py:282–286` — redundant `pass` statement + +```python +if self.is_child and self.passes == self.start_pass: + self.logger.pipeline_log("Skipping MPNN and Ranking steps ...") + pass # redundant — remove +``` + +The `pass` is a no-op. Remove it. Also the log message says "Skipping MPNN and +Ranking" but execution still continues into `s3` (fasta), `s4` (AF2), etc. — the +message is partially misleading. + +--- + +### `impress_manager.py:222` — `activity_summary` shows stale buffer count + +`len(self.new_pipeline_buffer)` is logged *after* `self.new_pipeline_buffer.clear()` +runs (line 217), so the buffered count is always reported as 0. Move the summary +log before the clear, or capture the count beforehand. + +--- + +### `impress_manager.py` — `self.flow` only exists after `start()` is called + +`submit_new_pipelines()` is a public method that references `self.flow`, but +`self.flow` is created inside `start()`. Calling `submit_new_pipelines()` directly +before `start()` raises `AttributeError`. Either initialise `self.flow = None` in +`__init__` with a guard, or make `submit_new_pipelines` private. diff --git a/src/impress/impress_manager.py b/src/impress/impress_manager.py index 8e52397..abb76b2 100644 --- a/src/impress/impress_manager.py +++ b/src/impress/impress_manager.py @@ -133,7 +133,7 @@ async def start( while True: any_activity: bool = False - completed_pipelines: list[ImpressBasePipeline] = [] + completed_pipelines: list[tuple] = [] for pipeline, pipeline_future in list(self.pipeline_tasks.items()): # Check if pipeline needs adaptive step and isn't already running one @@ -161,7 +161,7 @@ async def start( if getattr(pipeline, "kill_parent", False): self.logger.pipeline_killed(pipeline.name) pipeline_future.cancel() - completed_pipelines.append(pipeline) + completed_pipelines.append((pipeline, pipeline_future)) continue # Check if pipeline is done - but only mark as completed @@ -173,12 +173,12 @@ async def start( if not adaptive_task.done(): continue - completed_pipelines.append(pipeline) + completed_pipelines.append((pipeline, pipeline_future)) # Clean up completed pipelines - but only if their # adaptive tasks are also done actually_completed: list[ImpressBasePipeline] = [] - for pipeline in completed_pipelines: + for pipeline, future in completed_pipelines: # Double-check: only clean up if adaptive task is # done or doesn't exist if pipeline in self.adaptive_tasks: @@ -188,7 +188,16 @@ async def start( self.adaptive_tasks.pop(pipeline) self.pipeline_tasks.pop(pipeline, None) - self.logger.pipeline_completed(pipeline.name) + exc = None + if future.done() and not future.cancelled(): + try: + exc = future.exception() + except Exception: + pass + if exc is not None: + self.logger.pipeline_failed(pipeline.name, exc) + else: + self.logger.pipeline_completed(pipeline.name) actually_completed.append(pipeline) completed_pipelines = actually_completed diff --git a/src/impress/utils/logger.py b/src/impress/utils/logger.py index 642d699..200d2c9 100644 --- a/src/impress/utils/logger.py +++ b/src/impress/utils/logger.py @@ -136,6 +136,11 @@ def pipeline_completed(self, pipeline_name): message = f"Pipeline completed: {colored_name}" self.info(message, "manager") + def pipeline_failed(self, pipeline_name, exc): + colored_name = self._colorize(pipeline_name, Colors.BRIGHT_WHITE) + message = f"Pipeline FAILED: {colored_name} — {exc}" + self.error(message, "manager") + def pipeline_killed(self, pipeline_name): colored_name = self._colorize(pipeline_name, Colors.BRIGHT_WHITE) message = f"Pipeline killed: {colored_name}" From ef673cc27c782bdfdaf71c927e85d3d83d7185ae Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Sat, 29 Aug 2026 07:52:34 -0500 Subject: [PATCH 03/12] Add run_nonadaptive.py from main; update DragonExecutionBackend import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File was present in origin/main but absent from the ipdps_pdz_usecase base branch. Added with the same DragonExecutionBackendV3 → DragonExecutionBackend update applied to run_small_molecule_binding.py. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DbNxkFCwkCEEkHnN8xGH7Q --- .../small_molecule_binding/run_nonadaptive.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 examples/small_molecule_binding/run_nonadaptive.py diff --git a/examples/small_molecule_binding/run_nonadaptive.py b/examples/small_molecule_binding/run_nonadaptive.py new file mode 100644 index 0000000..55abfea --- /dev/null +++ b/examples/small_molecule_binding/run_nonadaptive.py @@ -0,0 +1,85 @@ +import asyncio +from typing import List + +from radical.asyncflow import LocalExecutionBackend +from concurrent.futures import ProcessPoolExecutor +from rhapsody.backends import DragonExecutionBackend + +from impress import ImpressManager, PipelineSetup +from small_molecule_binding import ( + SmallMoleculeBindingPipeline, + STEP_DONE, STEP_RFD3, STEP_MPNN, STEP_FASTRELAX, STEP_INTERFACE, STEP_AF2, +) + +import logging +import rhapsody +rhapsody.enable_logging(level=logging.DEBUG) + +# ── Per-step quality thresholds ──────────────────────────────────────────── +# These are passed to pipeline analysis tasks for metric logging but are not +# used by nonadaptive_decision to gate routing. +BACKBONE_MAX_CA_DEVIATION = 1.0 +BACKBONE_MIN_SS_FRACTION = 0.5 +FASTRELAX_MAX_FA_REP = 100.0 +FASTRELAX_MAX_SCORE = -250.0 +FASTRELAX_MAX_INTERACT = -8.0 +INTERFACE_MIN_SC = 0.55 +FOLD_MIN_PLDDT = 75.0 + + +async def nonadaptive_decision(pipeline: SmallMoleculeBindingPipeline) -> None: + step = pipeline.state.get('last_analysis_step') + routes = { + 'backbone': STEP_MPNN, + 'sequence': STEP_MPNN, + 'packmin': STEP_MPNN, + 'fastrelax': STEP_INTERFACE, + 'interface': STEP_AF2, + } + if step == 'fold': + pipeline.state['rfd3_input_pdb'] = None + pipeline.next_step = STEP_RFD3 + elif step in routes: + pipeline.next_step = routes[step] + else: + pipeline.logger.pipeline_log(f"[nonadaptive] Unknown step: {step!r}") + pipeline.next_step = STEP_DONE + + pipeline.logger.pipeline_log( + f"[nonadaptive/{step}] next_step={pipeline.next_step} " + f"ensemble={len(pipeline.state.get('ensemble', []))}" + ) + + +async def impress_smallmol_nonadaptive() -> None: + """Execute the small-molecule binding pipeline without adaptive routing.""" + #backend = await LocalExecutionBackend(ProcessPoolExecutor()) + backend = await DragonExecutionBackend() + manager: ImpressManager = ImpressManager(execution_backend=backend) + + pipeline_setups: List[PipelineSetup] = [ + PipelineSetup( + name=f"p{str(i)}", + type=SmallMoleculeBindingPipeline, + adaptive_fn=nonadaptive_decision, + kwargs={ + "backbone_max_ca_deviation": BACKBONE_MAX_CA_DEVIATION, + "backbone_min_ss_fraction": BACKBONE_MIN_SS_FRACTION, + "fastrelax_max_fa_rep": FASTRELAX_MAX_FA_REP, + "fastrelax_max_total_score": FASTRELAX_MAX_SCORE, + "fastrelax_max_interact": FASTRELAX_MAX_INTERACT, + "interface_min_sc": INTERFACE_MIN_SC, + "fold_min_plddt": FOLD_MIN_PLDDT, + "diffusion_batch_size": 4, + "num_refine_cycles": 2, + } + ) + for i in [1,2,4,6,7,8,10,11,12,13,14,15,16,18,19,20,23,26,27,30,32] + ] + + await manager.start(pipeline_setups=pipeline_setups) + await manager.flow.shutdown() + + +if __name__ == "__main__": + asyncio.run(impress_smallmol_nonadaptive()) From ae90d9bf738f0b4edc374448967858a5f75e3bb6 Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Sat, 29 Aug 2026 08:42:18 -0500 Subject: [PATCH 04/12] docs: update CODE_REVIEW.md files to reflect post-merge state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/: remove all protein_binding.py findings (file moved to examples/protein_binding/ in main merge); add note at header - examples/small_molecule_binding/: add run_nonadaptive.py to scope; extend debug-logging finding to cover both runner files; add findings for commented-out backend and hardcoded pipeline index list - examples/protein_binding/: full rewrite — new header (directory renamed protein_binding/), expanded scope to cover protein_binding.py (Boltz), protein_binding_run.py, run_nonadaptive.py; add findings for hardcoded Anvil MPNN_PATH, hardcoded peptide sequence, concurrent Boltz gather, unguarded os.unlink, DragonExecutionBackendV3 rename in all three runner files, stale delta_gpu_run.sh paths; remove resolved typo-directory finding and fixed log-message typo finding Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DbNxkFCwkCEEkHnN8xGH7Q --- examples/protein_binding/CODE_REVIEW.md | 174 +++++++++++++----- .../small_molecule_binding/CODE_REVIEW.md | 34 +++- src/CODE_REVIEW.md | 72 +------- 3 files changed, 158 insertions(+), 122 deletions(-) diff --git a/examples/protein_binding/CODE_REVIEW.md b/examples/protein_binding/CODE_REVIEW.md index d79c1ee..51a1251 100644 --- a/examples/protein_binding/CODE_REVIEW.md +++ b/examples/protein_binding/CODE_REVIEW.md @@ -1,8 +1,9 @@ -# Code Review — `examples/protien_binding_usecase/` +# Code Review — `examples/protein_binding/` **Date:** 2026-08-29 -**Scope:** `run_protein_binding.py`, `mpnn_wrapper.py`, -`plddt_extract_pipeline.py`, `af2_multimer_reduced.sh`, `delta_gpu_run.sh` +**Scope:** `protein_binding.py`, `protein_binding_run.py`, `run_protein_binding.py`, +`run_nonadaptive.py`, `mpnn_wrapper.py`, `plddt_extract_pipeline.py`, +`scripts/`, `delta_gpu_run.sh` --- @@ -36,29 +37,95 @@ avg_pae = running_sum / counter2 ``` `counter2` counts PAE matrix cells where exactly one of `row_index` / `col_index` -falls in `target_range`. If `target_range` is empty (protein shorter than 10 residues) -or the PAE matrix is empty, `counter2` stays 0 and this line raises `ZeroDivisionError`. +falls in `target_range`. If `target_range` is empty (protein shorter than 10 +residues) or the PAE matrix is empty, `counter2` stays 0 and this line raises +`ZeroDivisionError`. --- ## Potential Issues -### `run_protein_binding.py:50` — adaptive function reads CSV from current working directory +### `protein_binding.py:9` — MPNN path hardcoded to Anvil filesystem + +```python +MPNN_PATH = f"/anvil/projects/x-nairr240405/mason/ProteinMPNN" +``` + +This module-level constant silently resolves to a non-existent path on Delta or any +other system. The pipeline does accept `mpnn_path` as a constructor kwarg (line 37), +but callers who forget to supply it will get a confusing "directory not found" error +at task execution time instead of a clear startup failure. + +**Recommended fix:** default to `None` and raise a clear `ValueError` in `__init__` +if the path is not supplied and `MPNN_PATH` is not set in the environment. + +--- + +### `protein_binding.py:152` — hardcoded peptide sequence in `s3()` + +```python +pep_seq = "EGYQDYEPEA" # PDZ-domain peptide +``` + +This PDZ-specific constant is hardcoded inside `s3()`. It should be a constructor +parameter (e.g. `self.peptide_seq`) so the pipeline is reusable for other targets +without modifying the source. + +--- + +### `protein_binding.py:300` — all Boltz tasks launched concurrently + +```python +s4_results = await asyncio.gather(*alphafold_tasks, return_exceptions=True) +``` + +All structures are folded in parallel. With N structures, N Boltz processes compete +for GPU memory simultaneously, likely causing OOM on real runs. Folding should be +serialised per GPU or gated by a `asyncio.Semaphore`. + +--- + +### `protein_binding.py:220–221` — `os.unlink()` without existence check in `finalize()` + +```python +os.unlink(f"{self.output_path_af}/{a}.pdb") +os.unlink(f"{self.output_path}/af/fasta/{a}.fa") +``` + +If a Boltz task failed and the file was never created, `finalize()` raises +`FileNotFoundError`. Should use `pathlib.Path.unlink(missing_ok=True)` or check +existence first. + +--- + +### `protein_binding_run.py:7`, `run_protein_binding.py:6`, `run_nonadaptive.py:4` — old `DragonExecutionBackendV3` class name + +```python +from rhapsody.backends import DragonExecutionBackendV3 +``` + +The correct class is `DragonExecutionBackend` (the `V3` suffix was removed in a +recent rhapsody release). All three runner scripts still import the old name, causing +`ImportError` at startup on the current environment. Only `run_nonadaptive.py` in the +`small_molecule_binding` example was updated; the protein-binding runners were not. + +--- + +### `run_protein_binding.py:89` — adaptive function reads CSV from current working directory ```python file_name = f'af_stats_{pipeline.name}_pass_{pipeline.passes}.csv' with open(file_name) as fd: ``` -This path is relative to wherever the process was started (typically the script -directory). If the working directory is wrong or if AF2 fails and the file was never -written, this raises `FileNotFoundError` and crashes the adaptive function, leaving -the pipeline in an undefined state. Should use an absolute path based on `pipeline.base_path` -and wrap with `try/except`. +This path is relative to wherever the process was started. If the working directory +is wrong or if the stage failed and the file was never written, this raises +`FileNotFoundError` and crashes the adaptive function. Should use an absolute path +based on `pipeline.base_path`. --- -### `run_protein_binding.py:15` — `adaptive_criteria` declared `async` with no awaits +### `run_protein_binding.py:64` — `adaptive_criteria` declared `async` with no awaits ```python async def adaptive_criteria(current_score: float, previous_score: float) -> bool: @@ -72,15 +139,15 @@ overhead at every call site. Make it a plain `def`. ### `plddt_extract_pipeline.py` — incompatible with current IMPRESS output structure -`on_replica_done` in the current campaign (`sm_binding_workflow.py`) reads -`binder_scores_*.json` directly from the alphafold output directory. This script reads -from `af_pipeline_outputs_multi/{name}/af/prediction/best_models` — a directory tree -that the current pipeline does not create. This script is effectively stale and will +`on_replica_done` in the current campaign reads `binder_scores_*.json` directly from +the alphafold output directory. This script reads from +`af_pipeline_outputs_multi/{name}/af/prediction/best_models` — a directory tree that +the current pipeline does not create. This script is effectively stale and will produce empty / zero results if run against current outputs. --- -### `delta_gpu_run.sh:5,44` — SBATCH requests 4 tasks but runs single-node Dragon +### `delta_gpu_run.sh:3,44` — SBATCH requests 4 tasks but runs single-node Dragon ``` #SBATCH --tasks-per-node=4 @@ -106,29 +173,55 @@ directory to the dynamic linker search path — a security risk. Should be --- +### `delta_gpu_run.sh:32,35` — stale hardcoded path with directory typo + +```bash +IMPRESS_SCRIPTS_DIR="$SCRATCH/$USER/IMPRESS/examples/protien_binding_usecase" +WORKDIR="$SCRATCH/$USER/IMPRESS/examples/protien_binding_usecase" +``` + +The directory was renamed to `protein_binding` in the merge but these two path +variables still reference the old misspelled name. Any job submitted with this script +will `cd` to a non-existent directory and abort immediately. + +--- + ## Code Quality +### `protein_binding_run.py:17–18`, `run_nonadaptive.py:6,12` — rhapsody DEBUG logging enabled unconditionally + +```python +import rhapsody, logging +rhapsody.enable_logging(level=logging.DEBUG) +``` + +Both runner files enable DEBUG-level rhapsody logging unconditionally. On a +16-pipeline run this generates thousands of lines per second and buries application +output. Should default to INFO or be gated by an env variable. + +--- + ### `mpnn_wrapper.py` — massive duplicated subprocess.call blocks -The file's 140 lines handle 12+ scenario combinations (monomer/multimer × -fixed/unfixed × tied/homomer/interface) through deeply nested `if/else` with nearly -identical `subprocess.call` lists. The only differences are which `--*_jsonl` flags -are appended. Refactor to build the argument list incrementally and call +The file handles 12+ scenario combinations (monomer/multimer × fixed/unfixed × +tied/homomer/interface) through deeply nested `if/else` with nearly identical +`subprocess.call` lists. The only differences are which `--*_jsonl` flags are +appended. Refactor to build the argument list incrementally and call `subprocess.call` once. --- ### `mpnn_wrapper.py` — not used by the current pipeline -The active pipeline calls `mpnn.sh` → `mpnn_run.py` → LigandMPNN's `run.py`. -`mpnn_wrapper.py` calls `protein_mpnn_run.py` (ProteinMPNN, not LigandMPNN) via a -completely different argument schema. This file is dead code relative to the current -pipeline and should be either removed, labelled as a standalone utility, or replaced -with a properly integrated version. +The active pipeline calls `s1_mpnn.sh` → LigandMPNN's `run.py`. `mpnn_wrapper.py` +calls `protein_mpnn_run.py` (ProteinMPNN, not LigandMPNN) via a completely different +argument schema. This file is dead code relative to the current pipeline and should +be either removed, labelled as a standalone utility, or replaced with a properly +integrated version. --- -### `mpnn_wrapper.py:30-40` — `chains == None` instead of `chains is None` +### `mpnn_wrapper.py:30–40` — `chains == None` instead of `chains is None` ```python if chains == None: @@ -140,25 +233,15 @@ PEP 8 and Python convention require `if chains is None:`. The `==` form works fo --- -### `run_protein_binding.py:75` — typo in log message - -```python -pipeline.logger.pipeline_log(f'Adaptive descision: {decision}') -``` - -"descision" → "decision". - ---- - -### `run_protein_binding.py:39` — `eval` for venv activation +### `delta_gpu_run.sh:39` — `eval` for venv activation ```bash eval "$IMPRESS_PRE_EXEC" ``` -(in `delta_gpu_run.sh` line 39) Running an env-supplied string through `eval` allows -arbitrary code execution if `IMPRESS_PRE_EXEC` is set adversarially or incorrectly. -Replace with a direct `source` of the known venv path. +Running an env-supplied string through `eval` allows arbitrary code execution if +`IMPRESS_PRE_EXEC` is set adversarially or incorrectly. Replace with a direct +`source` of the known venv path. --- @@ -198,12 +281,3 @@ mkdir -p $WORK $UPPER These directories are never referenced in the apptainer call. Leftover from a prior overlay filesystem approach. Remove. - ---- - -### Directory name typo: `protien_binding_usecase` - -The directory is named `protien_binding_usecase` (protein misspelled). The companion -`IMPRESS/src/impress/pipelines/protein_binding.py` is spelled correctly. For new -branches, consider renaming to `protein_binding_usecase` to avoid confusion, although -this changes an established path. diff --git a/examples/small_molecule_binding/CODE_REVIEW.md b/examples/small_molecule_binding/CODE_REVIEW.md index 127fa00..16686e5 100644 --- a/examples/small_molecule_binding/CODE_REVIEW.md +++ b/examples/small_molecule_binding/CODE_REVIEW.md @@ -2,7 +2,7 @@ **Date:** 2026-08-29 **Scope:** `small_molecule_binding.py`, `run_small_molecule_binding.py`, -`run_test_small_molecule_binding.py`, `mock.py`, `scripts/` +`run_nonadaptive.py`, `run_test_small_molecule_binding.py`, `mock.py`, `scripts/` --- @@ -70,14 +70,16 @@ silently points to the wrong directory and the task reads stale or absent files. --- -### `run_small_molecule_binding.py:20` — rhapsody DEBUG logging enabled unconditionally +### `run_small_molecule_binding.py:20`, `run_nonadaptive.py:16` — rhapsody DEBUG logging enabled unconditionally ```python rhapsody.enable_logging(level=logging.DEBUG) ``` -DEBUG-level rhapsody logs every Dragon message exchange. On a 8-pipeline run this -generates thousands of lines per second and buries application output. +DEBUG-level rhapsody logs every Dragon message exchange. On a multi-pipeline run this +generates thousands of lines per second and buries application output. Both runner +scripts have this unconditional call; it should default to INFO or be gated by an +env variable. --- @@ -204,3 +206,27 @@ echo "[af2.sh] data_dir: $data_dir" Acceptable while debugging, but should be removed or made conditional on a `VERBOSE` flag before pushing to the shared branch. + +--- + +### `run_nonadaptive.py:56` — commented-out `LocalExecutionBackend` alternative + +```python +#backend = await LocalExecutionBackend(ProcessPoolExecutor()) +backend = await DragonExecutionBackend() +``` + +The commented-out local backend is a leftover from development/testing. Remove it to +avoid confusion about which backend is active. + +--- + +### `run_nonadaptive.py:77` — hardcoded pipeline index list + +```python +for i in [1,2,4,6,7,8,10,11,12,13,14,15,16,18,19,20,23,26,27,30,32] +``` + +Like `run_small_molecule_binding.py`'s hardcoded range, the specific protein indices +are baked in with no clear mapping to input files. Should be a named constant or +driven by scanning the input directory. diff --git a/src/CODE_REVIEW.md b/src/CODE_REVIEW.md index a8291ea..890b63b 100644 --- a/src/CODE_REVIEW.md +++ b/src/CODE_REVIEW.md @@ -2,7 +2,10 @@ **Date:** 2026-08-29 **Scope:** `/scratch/bblj/mgoliyad1/IMPRESS/src/impress/` -**Status:** bugs marked ✅ fixed or ⚠️ open +**Status:** bugs marked ✅ fixed or ⚠️ open +**Note:** `protein_binding.py` was moved from `src/impress/pipelines/` to +`examples/protein_binding/` in the `origin/main` merge; findings for that file +are now in `examples/protein_binding/CODE_REVIEW.md`. --- @@ -24,22 +27,6 @@ raises `ValueError: not enough values to unpack` the moment any pipeline is kill --- -### ⚠️ `protein_binding.py:7–9` — module-level `EnvironmentError` on import - -`MPNN_PATH` is validated at module import time: - -```python -_mpnn = os.environ.get("MPNN_PATH") -if not _mpnn: - raise EnvironmentError("MPNN_PATH is not set ...") -``` - -Any code that does `from impress.pipelines.protein_binding import ...` — even -conditionally — will crash at import if the env var is absent. Should be deferred -to `__init__`. - ---- - ## Potential Issues ### `impress_manager.py` — `WorkflowEngine` leaks on exception @@ -76,18 +63,6 @@ implementation, or declare it consistently as sync across the hierarchy. --- -### `protein_binding.py:303–310` — all AF2 tasks launched concurrently - -```python -results = await asyncio.gather(*alphafold_tasks, return_exceptions=True) -``` - -All structures are folded in parallel. If there are N structures, N AF2 processes -compete for GPU memory simultaneously, likely causing OOM on real runs. AF2 should -be serialised per GPU or gated by a semaphore. - ---- - ## Code Quality ### `logger.py` — no log level filtering @@ -107,45 +82,6 @@ sets a custom output stream (e.g. for testing) will silently lose error messages --- -### `protein_binding.py:185` — hardcoded peptide sequence - -```python -pep_seq = "EGYQDYEPEA" # PDZ-domain peptide -``` - -This is a PDZ-specific constant hardcoded inside `s3()`. It should be a -constructor parameter (e.g. `self.peptide_seq`) so the pipeline is reusable for -other targets. - ---- - -### `protein_binding.py:267–268` — `os.unlink()` without existence check in `finalize()` - -```python -os.unlink(f"{self.output_path_af}/{a}.pdb") -os.unlink(f"{self.output_path}/af/fasta/{a}.fa") -``` - -If an AF2 task failed and the file was never created, `finalize()` raises -`FileNotFoundError`. Should use `pathlib.Path.unlink(missing_ok=True)` or check -existence first. - ---- - -### `protein_binding.py:282–286` — redundant `pass` statement - -```python -if self.is_child and self.passes == self.start_pass: - self.logger.pipeline_log("Skipping MPNN and Ranking steps ...") - pass # redundant — remove -``` - -The `pass` is a no-op. Remove it. Also the log message says "Skipping MPNN and -Ranking" but execution still continues into `s3` (fasta), `s4` (AF2), etc. — the -message is partially misleading. - ---- - ### `impress_manager.py:222` — `activity_summary` shows stale buffer count `len(self.new_pipeline_buffer)` is logged *after* `self.new_pipeline_buffer.clear()` From 1cc2cfce7bae550659521387ad3d5a8c361d1f47 Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Sun, 30 Aug 2026 05:53:21 -0500 Subject: [PATCH 05/12] fix: apply all CODE_REVIEW.md findings across src/, small_molecule_binding/, protein_binding/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugs fixed: - filter_shape.py: undefined sfxn → sfxn_clean in RosettaScripts XML; add argparse - packmin.py: rosetta.* → pyrosetta.rosetta.* imports; remove commented dead code - mpnn_wrapper.py (pb): #!/bin/sh shebang → #!/usr/bin/env python3 - mpnn_wrapper.py (pb): temperature type=int → type=float - impress_manager.py: WorkflowEngine shutdown in try/finally; flow=None in __init__; guard submit_new_pipelines - impress_pipeline.py: remove @abstractmethod from finalize; add no-op base impl Potential issues: - protein_binding.py, small_molecule_binding.py: Anvil hardcoded paths → None + ValueError - protein_binding.py: os.unlink → Path.unlink(missing_ok=True) - protein_binding.py: Boltz gather gated with Semaphore(2) via _guarded_s4 - protein_binding_run.py, run_protein_binding.py, run_nonadaptive.py (pb): DragonExecutionBackendV3 → DragonExecutionBackend - run_protein_binding.py: adaptive_criteria async→def; CSV path uses pipeline.base_path - delta_gpu_run.sh: path typo protien→protein; LD_LIBRARY_PATH guarded; tasks-per-node=1; eval→source Code quality: - run_small_molecule_binding.py: DEBUG→INFO logging; remove unused ThreadPoolExecutor imports - run_nonadaptive.py (smb): DEBUG→INFO; remove commented-out LocalExecutionBackend import - protein_binding_run.py, run_nonadaptive.py (pb): DEBUG→INFO logging - filter_shape.py: remove redundant .close() in with blocks - filter_energy.py: remove unconditional print to stdout - fastrelax.sh: quote $0 in dirname - af2.sh: remove diagnostic echo lines - af2_multimer_reduced.sh: remove unused /tmp/work /tmp/upper - rfd3.sh: fix comment arg order ($4=diffusion_batch_size $5=scaffold_arg) - small_molecule_binding.py: remove dead fixed_residues_file comment - mpnn_wrapper.py (pb): chains==None → chains is None - impress_manager.py: log buffered count before clear (fix stale-0 bug) - logger.py: add min_level filtering; fix error()/critical() to use output_stream Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DbNxkFCwkCEEkHnN8xGH7Q --- .../protein_binding/af2_multimer_reduced.sh | 5 - examples/protein_binding/delta_gpu_run.sh | 13 +- examples/protein_binding/mpnn_wrapper.py | 6 +- examples/protein_binding/protein_binding.py | 22 +- .../protein_binding/protein_binding_run.py | 8 +- examples/protein_binding/run_nonadaptive.py | 6 +- .../protein_binding/run_protein_binding.py | 9 +- .../small_molecule_binding/run_nonadaptive.py | 5 +- .../run_small_molecule_binding.py | 4 +- .../small_molecule_binding/scripts/af2.sh | 2 - .../scripts/fastrelax.sh | 2 +- .../scripts/filter_energy.py | 2 - .../scripts/filter_shape.py | 93 ++++--- .../small_molecule_binding/scripts/packmin.py | 23 +- .../small_molecule_binding/scripts/rfd3.sh | 4 +- .../small_molecule_binding.py | 13 +- src/impress/impress_manager.py | 230 +++++++++--------- src/impress/pipelines/impress_pipeline.py | 1 - src/impress/utils/logger.py | 55 +++-- 19 files changed, 246 insertions(+), 257 deletions(-) diff --git a/examples/protein_binding/af2_multimer_reduced.sh b/examples/protein_binding/af2_multimer_reduced.sh index 3091556..75e944b 100644 --- a/examples/protein_binding/af2_multimer_reduced.sh +++ b/examples/protein_binding/af2_multimer_reduced.sh @@ -7,11 +7,6 @@ set -e set -x -# work and upperdir need to be on same file system -WORK=/tmp/work -UPPER=/tmp/upper -mkdir -p $WORK $UPPER - export XLA_PYTHON_CLIENT_PREALLOCATE="false" export XLA_PYTHON_CLIENT_MEM_FRACTION=".75" export XLA_PYTHON_CLIENT_ALLOCATOR="platform" diff --git a/examples/protein_binding/delta_gpu_run.sh b/examples/protein_binding/delta_gpu_run.sh index d69a637..9ccd267 100644 --- a/examples/protein_binding/delta_gpu_run.sh +++ b/examples/protein_binding/delta_gpu_run.sh @@ -1,7 +1,7 @@ #!/bin/bash #SBATCH --partition=gpuA40x4 #SBATCH --nodes=1 -#SBATCH --tasks-per-node=4 +#SBATCH --tasks-per-node=1 #SBATCH --cpus-per-task=16 #SBATCH --gpus=4 #SBATCH --exclusive @@ -18,25 +18,24 @@ set -e export CUDA_HOME=/opt/nvidia/hpc_sdk/Linux_x86_64/25.3/cuda/12.8 export MPI_LIB=/opt/cray/pe/mpich/8.1.32/ofi/gnu/11.2/lib-abi-mpich export FAB_LIB=/opt/cray/libfabric/1.22.0/lib64 -export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH} +export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH:-} # ── Paths — edit these when moving to a different system ───────────────────── : "${SCRATCH:?SCRATCH is not set (e.g. export SCRATCH=/scratch/bblj)}" export MPNN_PATH="$SCRATCH/$USER/ProteinMPNN" -# Switch activation command below if using conda instead of venv -IMPRESS_PRE_EXEC="source $HOME/ve/impress/bin/activate" +IMPRESS_VENV="${IMPRESS_VENV:-$HOME/ve/impress}" export AF2_DATABASE="/scratch/rhaas/SUP-5301/database" export AF2_SIF="/scratch/rhaas/SUP-5301/alphafold.sif" export IMPRESS_INPUT_DIR="$SCRATCH/$USER/IMPRESS_inputs/prod_in" export IMPRESS_OUTPUT_DIR="$SCRATCH/$USER/IMPRESS_outputs" -export IMPRESS_SCRIPTS_DIR="$SCRATCH/$USER/IMPRESS/examples/protien_binding_usecase" +export IMPRESS_SCRIPTS_DIR="$SCRATCH/$USER/IMPRESS/examples/protein_binding" # ── Working directory ───────────────────────────────────────────────────────── -WORKDIR="$SCRATCH/$USER/IMPRESS/examples/protien_binding_usecase" +WORKDIR="$SCRATCH/$USER/IMPRESS/examples/protein_binding" cd "$WORKDIR" mkdir -p logs -eval "$IMPRESS_PRE_EXEC" +source "${IMPRESS_VENV}/bin/activate" dragon-config add --ofi-runtime-lib="${FAB_LIB}" # ── Run ─────────────────────────────────────────────────────────────────────── diff --git a/examples/protein_binding/mpnn_wrapper.py b/examples/protein_binding/mpnn_wrapper.py index 1e8942d..cf3c475 100644 --- a/examples/protein_binding/mpnn_wrapper.py +++ b/examples/protein_binding/mpnn_wrapper.py @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env python3 import argparse parser = argparse.ArgumentParser() import subprocess @@ -15,7 +15,7 @@ parser.add_argument("-homo", "--homo", help="Are your input files homomers? (0 or 1) Note: Overrides tied positions. If homomer is specified, all positions on designed chains will be tied. If positions are restricted, lists must be of same length. Example: -index='1 3 5 7, 1 3 5 7'", type=int) parser.add_argument("-bias_AA", "--bias_AA", help="For which amino acids would you like to install bias? Example: -bias_AA='D E H'", type=str) parser.add_argument("-bias_weight", "--bias_weight", help="What weights would you like to install for the biased amino acids? Lists must match in length. Example: -bias_weight='0.3 -0.3 0.5'", type=str) -parser.add_argument("-temp", "--temp", help="What temperature would you like to sample from? Example: 0.3", type=int, default=0.1) +parser.add_argument("-temp", "--temp", help="What temperature would you like to sample from? Example: 0.3", type=float, default=0.1) parser.add_argument("-inter", "--interface", help="Would you like to design the interface? Do not specify indices if designing the interface. (1 or 0)", type=int, default=0) @@ -27,7 +27,7 @@ mpnn_path=args.mpnn_path is_monomer=args.is_monomer #default is_monomer false chains=args.design_chains -if chains == None: +if chains is None: chains='A' #default design chain A index=args.index fix=args.fix #default false, specify non fixed diff --git a/examples/protein_binding/protein_binding.py b/examples/protein_binding/protein_binding.py index 64ca80d..15bde0d 100644 --- a/examples/protein_binding/protein_binding.py +++ b/examples/protein_binding/protein_binding.py @@ -6,7 +6,7 @@ from impress.pipelines.impress_pipeline import ImpressBasePipeline -MPNN_PATH = f"/anvil/projects/x-nairr240405/mason/ProteinMPNN" +MPNN_PATH = os.environ.get("MPNN_PATH", "") _BOLTZ_CHAIN_MAP = {'pdz': 'A', 'pep': 'B'} @@ -34,7 +34,9 @@ def __init__(self, name, flow, configs=None, **kwargs): self.num_seqs = kwargs.get("num_seqs", 10) self.sub_order = kwargs.get("sub_order", 0) self.max_passes = kwargs.get("max_passes", 10) - self.mpnn_path = kwargs.get("mpnn_path", MPNN_PATH) + self.mpnn_path = kwargs.get("mpnn_path") or MPNN_PATH + if not self.mpnn_path: + raise ValueError("mpnn_path must be supplied via kwarg or MPNN_PATH env var") # Sequence and score state self.current_scores = {} @@ -217,10 +219,11 @@ async def get_scores_map(self): def finalize(self, sub_iter_seqs): # finalize the "cleanup" of the current pipeline + from pathlib import Path for a in sub_iter_seqs: self.fasta_list_2.remove(f"{a}.pdb") - os.unlink(f"{self.output_path_af}/{a}.pdb") - os.unlink(f"{self.output_path}/af/fasta/{a}.fa") + Path(f"{self.output_path_af}/{a}.pdb").unlink(missing_ok=True) + Path(f"{self.output_path}/af/fasta/{a}.fa").unlink(missing_ok=True) self.previous_scores = copy.deepcopy(self.current_scores) async def run(self): @@ -255,6 +258,13 @@ async def run(self): alphafold_tasks = [] post_exec_tasks = [] + # Limit concurrent Boltz launches to avoid GPU OOM. + _boltz_sem = asyncio.Semaphore(2) + + async def _guarded_s4(target_fasta): + async with _boltz_sem: + return await self.s4(target_fasta=target_fasta) + for target_fasta in fasta_files: models_path = os.path.join( self.output_path, "af", "prediction", "dimer_models", target_fasta, @@ -282,8 +292,8 @@ async def run(self): f"{target_fasta}.pdb", ) - # launch coroutine without awaiting yet - alphafold_tasks.append(self.s4(target_fasta=target_fasta)) + # launch coroutine without awaiting yet (semaphore-gated) + alphafold_tasks.append(_guarded_s4(target_fasta)) post_exec_tasks.append( self.s4_post_exec( target_fasta=target_fasta, diff --git a/examples/protein_binding/protein_binding_run.py b/examples/protein_binding/protein_binding_run.py index fa9e4b9..bacbd3c 100644 --- a/examples/protein_binding/protein_binding_run.py +++ b/examples/protein_binding/protein_binding_run.py @@ -11,10 +11,9 @@ import numpy as np import pandas as pd -# from rhapsody.backends import DragonExecutionBackendV3 -from radical.asyncflow import LocalExecutionBackend +from rhapsody.backends import DragonExecutionBackend import rhapsody -rhapsody.enable_logging(level=logging.DEBUG) +rhapsody.enable_logging(level=logging.INFO) from impress import PipelineSetup from impress import ImpressManager @@ -326,8 +325,7 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> None: async def impress_protein_bind() -> None: """Execute protein binding analysis with LLM-driven adaptive optimization.""" -# backend = await DragonExecutionBackendV3() - backend = await LocalExecutionBackend(ProcessPoolExecutor()) + backend = await DragonExecutionBackend() manager: ImpressManager = ImpressManager(execution_backend=backend) diff --git a/examples/protein_binding/run_nonadaptive.py b/examples/protein_binding/run_nonadaptive.py index 6eea85e..6a97fe7 100644 --- a/examples/protein_binding/run_nonadaptive.py +++ b/examples/protein_binding/run_nonadaptive.py @@ -1,7 +1,7 @@ import asyncio from typing import List -from rhapsody.backends import DragonExecutionBackendV3 +from rhapsody.backends import DragonExecutionBackend from rhapsody.telemetry import define_event from impress import PipelineSetup @@ -9,7 +9,7 @@ from protein_binding import ProteinBindingPipeline import rhapsody, logging -rhapsody.enable_logging(level=logging.DEBUG) +rhapsody.enable_logging(level=logging.INFO) def _on_task_event(event) -> None: @@ -19,7 +19,7 @@ def _on_task_event(event) -> None: async def impress_protein_bind_nonadaptive() -> None: - backend = await DragonExecutionBackendV3() + backend = await DragonExecutionBackend() manager: ImpressManager = ImpressManager( execution_backend=backend, diff --git a/examples/protein_binding/run_protein_binding.py b/examples/protein_binding/run_protein_binding.py index cf66937..b006f5c 100644 --- a/examples/protein_binding/run_protein_binding.py +++ b/examples/protein_binding/run_protein_binding.py @@ -1,9 +1,10 @@ import copy +import os import shutil import asyncio from typing import Dict, Any, Optional, List -from rhapsody.backends import DragonExecutionBackendV3 +from rhapsody.backends import DragonExecutionBackend from rhapsody.telemetry import define_event from rhapsody.telemetry.events import make_event @@ -61,12 +62,12 @@ def _on_task_event(event) -> None: # Adaptive helpers # --------------------------------------------------------------------------- -async def adaptive_criteria(current_score: float, previous_score: float) -> bool: +def adaptive_criteria(current_score: float, previous_score: float) -> bool: return current_score > previous_score async def impress_protein_bind() -> None: - backend = await DragonExecutionBackendV3() + backend = await DragonExecutionBackend() manager: ImpressManager = ImpressManager( execution_backend=backend, @@ -85,7 +86,7 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[s sid = tel.session_id if tel else None # Read current scores from CSV - file_name = f'af_stats_{pipeline.name}_pass_{pipeline.passes}.csv' + file_name = os.path.join(pipeline.base_path, f'af_stats_{pipeline.name}_pass_{pipeline.passes}.csv') with open(file_name) as fd: for line in fd.readlines()[1:]: line = line.strip() diff --git a/examples/small_molecule_binding/run_nonadaptive.py b/examples/small_molecule_binding/run_nonadaptive.py index 55abfea..bc2dd76 100644 --- a/examples/small_molecule_binding/run_nonadaptive.py +++ b/examples/small_molecule_binding/run_nonadaptive.py @@ -1,8 +1,6 @@ import asyncio from typing import List -from radical.asyncflow import LocalExecutionBackend -from concurrent.futures import ProcessPoolExecutor from rhapsody.backends import DragonExecutionBackend from impress import ImpressManager, PipelineSetup @@ -13,7 +11,7 @@ import logging import rhapsody -rhapsody.enable_logging(level=logging.DEBUG) +rhapsody.enable_logging(level=logging.INFO) # ── Per-step quality thresholds ──────────────────────────────────────────── # These are passed to pipeline analysis tasks for metric logging but are not @@ -53,7 +51,6 @@ async def nonadaptive_decision(pipeline: SmallMoleculeBindingPipeline) -> None: async def impress_smallmol_nonadaptive() -> None: """Execute the small-molecule binding pipeline without adaptive routing.""" - #backend = await LocalExecutionBackend(ProcessPoolExecutor()) backend = await DragonExecutionBackend() manager: ImpressManager = ImpressManager(execution_backend=backend) diff --git a/examples/small_molecule_binding/run_small_molecule_binding.py b/examples/small_molecule_binding/run_small_molecule_binding.py index 5f9b509..41df18d 100644 --- a/examples/small_molecule_binding/run_small_molecule_binding.py +++ b/examples/small_molecule_binding/run_small_molecule_binding.py @@ -1,9 +1,7 @@ import asyncio import os -from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor from typing import List -from radical.asyncflow import LocalExecutionBackend from rhapsody.backends import DragonExecutionBackend from impress import ImpressManager, PipelineSetup @@ -17,7 +15,7 @@ import logging import rhapsody -rhapsody.enable_logging(level=logging.DEBUG) +rhapsody.enable_logging(level=logging.INFO) # ── Per-step quality thresholds ──────────────────────────────────────────── BACKBONE_MAX_CA_DEVIATION = 1.0 diff --git a/examples/small_molecule_binding/scripts/af2.sh b/examples/small_molecule_binding/scripts/af2.sh index 59c4be3..2f051d1 100755 --- a/examples/small_molecule_binding/scripts/af2.sh +++ b/examples/small_molecule_binding/scripts/af2.sh @@ -28,12 +28,10 @@ if [ -z "$colabfold_bin" ]; then echo "ERROR: colabfold_batch not found in venv or at $colabfold_path" >&2 exit 1 fi -echo "[af2.sh] using colabfold_batch: $colabfold_bin" # Use scratch for the model weights cache to avoid home quota exhaustion. # COLABFOLD_CACHE_DIR must be set (delta_sbatch.sh exports it). data_dir="${COLABFOLD_CACHE_DIR:-${HOME}/.cache/colabfold}" -echo "[af2.sh] data_dir: $data_dir" "$colabfold_bin" \ --model-type alphafold2 \ diff --git a/examples/small_molecule_binding/scripts/fastrelax.sh b/examples/small_molecule_binding/scripts/fastrelax.sh index 5d845f3..729586c 100755 --- a/examples/small_molecule_binding/scripts/fastrelax.sh +++ b/examples/small_molecule_binding/scripts/fastrelax.sh @@ -8,7 +8,7 @@ pdb_path="$1" lig_path="$2" output_dir="$3" -SCRIPT_DIR="$(dirname $0)" +SCRIPT_DIR="$(dirname "$0")" source "${ENV_DIR:-/u/${USER}/ve/impress}/bin/activate" diff --git a/examples/small_molecule_binding/scripts/filter_energy.py b/examples/small_molecule_binding/scripts/filter_energy.py index 4acb46d..a570cd8 100644 --- a/examples/small_molecule_binding/scripts/filter_energy.py +++ b/examples/small_molecule_binding/scripts/filter_energy.py @@ -42,8 +42,6 @@ parts = line.split() ligand_energy = float(parts[-1]) # The last element is the total energy - print(f"Processed: {pdb_file}, Ligand Energy ({ligand_name}): {ligand_energy}") - # Check if the ligand energy is negative if ligand_energy < 0: with open(output_file, 'a') as of: diff --git a/examples/small_molecule_binding/scripts/filter_shape.py b/examples/small_molecule_binding/scripts/filter_shape.py index fcd8d92..cfb8ab7 100644 --- a/examples/small_molecule_binding/scripts/filter_shape.py +++ b/examples/small_molecule_binding/scripts/filter_shape.py @@ -1,30 +1,37 @@ +import argparse import os import re import pyrosetta -import sys -pdb_directory = sys.argv[1] # '/WWW/PDB_Files' -SC_output_file = sys.argv[2] # 'shape_complementarity_values.txt' -ligand_name = sys.argv[3] # 'ALR' -gen_output_file = sys.argv[4] # 'interface_values.txt' -pyrosetta.init(f"-ignore_unrecognized_res -ignore_zero_occupancy --extra_res_fa {ligand_name}.params -corrections::beta_nov16 true -mute all") +def parse_args(): + parser = argparse.ArgumentParser(description="Rosetta shape-complementarity/interface analysis") + parser.add_argument("pdb_directory", help="Directory of PDB files to analyse") + parser.add_argument("SC_output_file", help="Output file for shape-complementarity values") + parser.add_argument("ligand_name", help="Ligand residue name (e.g. ALR)") + parser.add_argument("gen_output_file", help="Output CSV for interface metrics") + return parser.parse_args() -# Define directories and files -#pdb_directory = '/WWW/PDB_Files' -#SC_output_file = 'shape_complementarity_values.txt' -# Set up the general output file which will have metrics that look at the interface -with open(gen_output_file, 'w') as genout: - # Write the shape complementarity value to the output file - genout.write(f"FileName,Shape Complementarity,ddg,contact molecular surf,SASA,Very buried unsat hbond,Surface unsat hbond,SAP SCORE\n") - genout.close() +def main(): + args = parse_args() -# Create list of PDB files to analyze -pdb_files = [f for f in os.listdir(pdb_directory) if f.endswith('.pdb')] + pyrosetta.init( + f"-ignore_unrecognized_res -ignore_zero_occupancy --extra_res_fa {args.ligand_name}.params" + f" -corrections::beta_nov16 true -mute all" + ) -################################################################################################################################################################################################################################### -protocol = pyrosetta.rosetta.protocols.rosetta_scripts.XmlObjects().create_from_string( + # Set up the general output file which will have metrics that look at the interface + with open(args.gen_output_file, 'w') as genout: + genout.write( + "FileName,Shape Complementarity,ddg,contact molecular surf,SASA," + "Very buried unsat hbond,Surface unsat hbond,SAP SCORE\n" + ) + + # Create list of PDB files to analyze + pdb_files = [f for f in os.listdir(args.pdb_directory) if f.endswith('.pdb')] + + protocol = pyrosetta.rosetta.protocols.rosetta_scripts.XmlObjects().create_from_string( """ @@ -34,7 +41,7 @@ - + @@ -93,41 +100,25 @@ """).get_mover("ParsedProtocol") -#################################################################################################################################################################################################################################### - - -##print(f""" -##shape complementarity : {pose.scores['sc2']} -##ddg : {pose.scores['ddg']} -##contact molecular surf : {pose.scores['cms']} -##SASA : {pose.scores['IA_dSASA_int']} -##Very buried unsat hbond: {pose.scores['vbuns']} -##Surface unsat hbond : {pose.scores['sbuns']} -##SAP SCORE : {pose.scores['sap_score']} -##""") - -# Analyze the selected PDB files -for pdb_file in pdb_files: - full_path = os.path.join(pdb_directory, pdb_file) - - # Initialize variables for ligand energy - pose = pyrosetta.pose_from_pdb(full_path) - protocol.apply(pose) + # Analyze the selected PDB files + for pdb_file in pdb_files: + full_path = os.path.join(args.pdb_directory, pdb_file) - # Open the SC output file - with open(SC_output_file, 'a') as SCout: - - # Write the shape complementarity value to the output file - SCout.write(f"{pdb_file}\tShape Complementarity: {pose.scores['sc2']}\n") - SCout.close() + pose = pyrosetta.pose_from_pdb(full_path) + protocol.apply(pose) - # Open the general output file - with open(gen_output_file, 'a') as genout: + with open(args.SC_output_file, 'a') as SCout: + SCout.write(f"{pdb_file}\tShape Complementarity: {pose.scores['sc2']}\n") - # Write the shape complementarity value to the output file - genout.write(f"{pdb_file},{pose.scores['sc2']},{pose.scores['ddg']},{pose.scores['cms']},{pose.scores['IA_dSASA_int']},{pose.scores['vbuns']},{pose.scores['sbuns']},{pose.scores['sap_score']}\n") - genout.close() + with open(args.gen_output_file, 'a') as genout: + genout.write( + f"{pdb_file},{pose.scores['sc2']},{pose.scores['ddg']}," + f"{pose.scores['cms']},{pose.scores['IA_dSASA_int']}," + f"{pose.scores['vbuns']},{pose.scores['sbuns']},{pose.scores['sap_score']}\n" + ) -print(f"Shape complementarity values have been written to {SC_output_file}.") + print(f"Shape complementarity values have been written to {args.SC_output_file}.") +if __name__ == "__main__": + main() diff --git a/examples/small_molecule_binding/scripts/packmin.py b/examples/small_molecule_binding/scripts/packmin.py index 04fefc8..f417ec8 100644 --- a/examples/small_molecule_binding/scripts/packmin.py +++ b/examples/small_molecule_binding/scripts/packmin.py @@ -7,21 +7,11 @@ from pyrosetta.teaching import * #Core Includes -from rosetta.core.kinematics import MoveMap -from rosetta.core.kinematics import FoldTree -from rosetta.core.pack.task import TaskFactory -from rosetta.core.pack.task import operation -from rosetta.core.simple_metrics import metrics -from rosetta.core.select import residue_selector as selections -from rosetta.core import select -from rosetta.core.select.movemap import * +from pyrosetta.rosetta.core.pack.task import operation +from pyrosetta.rosetta.core.select.movemap import * #Protocol Includes -from rosetta.protocols import minimization_packing as pack_min -from rosetta.protocols import relax as rel -from rosetta.protocols.antibody.residue_selector import CDRResidueSelector -from rosetta.protocols.antibody import * -from rosetta.protocols.loops import * +from pyrosetta.rosetta.protocols import minimization_packing as pack_min ''' When downloading a new PDB file, do a pack_min minimization with coordinate constraints and a ligand. @@ -93,18 +83,12 @@ def main(args): # Packer tasks with -ex1 and -ex2 tf = ut.make_task_factory() - #From pack_min tutorial from jupyter notebooks - ###tf = TaskFactory() tf.push_back(operation.InitializeFromCommandline()) tf.push_back(operation.RestrictToRepacking()) packer = pack_min.PackRotamersMover() packer.task_factory(tf) - #This line is from khare lab relax code, not sure if this will be necessary: - #pp = Pose(pose) - - #Run the packer. (Note this may take a few minutes) packer.apply(pose) # Write Rosetta score JSON alongside the output PDB @@ -115,7 +99,6 @@ def main(args): #Dump the PDB pose.dump_pdb(out_name) - ##pose.dump_pdb('/outputs/2r0l_all_repack.pdb') if __name__ == '__main__': args = parse_args() diff --git a/examples/small_molecule_binding/scripts/rfd3.sh b/examples/small_molecule_binding/scripts/rfd3.sh index b75bb26..72d46bd 100755 --- a/examples/small_molecule_binding/scripts/rfd3.sh +++ b/examples/small_molecule_binding/scripts/rfd3.sh @@ -2,8 +2,8 @@ set -euo pipefail # Backbone generation via RFDiffusion3 (apptainer) -# Args: $1=foundry_sif_path $2=output_dir $3=inputs $4=scaffold_arg $5=diffusion_batch_size -# scaffold_arg: "scaffoldguided.target_pdb=" or "" if unused +# Args: $1=foundry_sif_path $2=output_dir $3=inputs $4=diffusion_batch_size $5=scaffold_arg +# scaffold_arg: "scaffoldguided.target_pdb=" or "" if unused (optional, defaults to "") foundry_sif_path="$1" output_dir="$2" diff --git a/examples/small_molecule_binding/small_molecule_binding.py b/examples/small_molecule_binding/small_molecule_binding.py index e8d86a5..3367c23 100644 --- a/examples/small_molecule_binding/small_molecule_binding.py +++ b/examples/small_molecule_binding/small_molecule_binding.py @@ -146,11 +146,17 @@ def __init__(self, name, flow, configs=None, **kwargs): "scripts_path", os.path.join(self.base_path, "scripts") ) self.pipeline_inputs = os.path.join(self.base_path, f"{self.name}_in") - self.mpnn_dir = kwargs.get("mpnn_dir", f"/anvil/projects/x-nairr240405/mason/LigandMPNN") + self.mpnn_dir = kwargs.get("mpnn_dir") or os.environ.get("MPNN_DIR") + if not self.mpnn_dir: + raise ValueError("mpnn_dir must be supplied via kwarg or MPNN_DIR env var") # Configurable tool paths and ensemble sizes - self.foundry_sif_path = kwargs.get("foundry_sif_path", "/anvil/projects/x-nairr240405/mason/foundry.sif") - self.colabfold_path = kwargs.get("colabfold_path", "/anvil/projects/x-nairr240405/mason/localcolabfold") + self.foundry_sif_path = kwargs.get("foundry_sif_path") or os.environ.get("FOUNDRY_SIF_PATH") + if not self.foundry_sif_path: + raise ValueError("foundry_sif_path must be supplied via kwarg or FOUNDRY_SIF_PATH env var") + self.colabfold_path = kwargs.get("colabfold_path") or os.environ.get("COLABFOLD_PATH") + if not self.colabfold_path: + raise ValueError("colabfold_path must be supplied via kwarg or COLABFOLD_PATH env var") self.ligand_params = kwargs.get("ligand_params", "ALR.params") self.mpnn_ensemble_size = kwargs.get("mpnn_ensemble_size", 1) self.num_refine_cycles = kwargs.get("num_refine_cycles", 3) @@ -688,7 +694,6 @@ async def _run_refine_cycle(self): self.logger.pipeline_log(f"running mpnn [cycle {cycle_i}]") await self.mpnn() -# fixed_residues_file=f"{self.pipeline_inputs}/fixed_residues.txt" ) self.logger.pipeline_log(f"mpnn [cycle {cycle_i}] finished") await self.analysis_sequence() await self.run_adaptive_step() diff --git a/src/impress/impress_manager.py b/src/impress/impress_manager.py index f08951f..ec8f3de 100644 --- a/src/impress/impress_manager.py +++ b/src/impress/impress_manager.py @@ -43,6 +43,7 @@ def __init__( self._telemetry_config: dict[str, Any] = telemetry_config or {} self._telemetry_subscribers: list[Callable] = telemetry_subscribers or [] self.telemetry: Any = None + self.flow: Optional[WorkflowEngine] = None def _normalize_pipeline_setup( self, setup: Union[dict[str, Any], PipelineSetup] @@ -77,6 +78,8 @@ def submit_new_pipelines( ValueError: If pipeline type is not a subclass of ImpressBasePipeline """ + if self.flow is None: + raise RuntimeError("ImpressManager.start() must be called before submit_new_pipelines()") for setup_input in pipeline_setups: # Normalize to PipelineSetup object setup = self._normalize_pipeline_setup(setup_input) @@ -136,122 +139,129 @@ async def start( """ self.logger.separator("IMPRESS MANAGER STARTING") - self.flow: WorkflowEngine = await WorkflowEngine.create( + self.flow = await WorkflowEngine.create( backend=self.execution_backend ) - if self._telemetry_config: - self.telemetry = await self.flow.start_telemetry(**self._telemetry_config) - for fn in self._telemetry_subscribers: - self.telemetry.subscribe(fn) - - self.logger.manager_starting(len(pipeline_setups)) - - self.submit_new_pipelines(pipeline_setups) - - while True: - any_activity: bool = False - completed_pipelines: list[tuple] = [] - - for pipeline, pipeline_future in list(self.pipeline_tasks.items()): - # Check if pipeline needs adaptive step and isn't already running one - if ( - getattr(pipeline, "invoke_adaptive_step", False) - and pipeline not in self.adaptive_tasks - ): - adaptive_task: asyncio.Task = asyncio.create_task( - self._run_adaptive_fn(pipeline) - ) - self.adaptive_tasks[pipeline] = adaptive_task - any_activity = True - - # Check if pipeline has new config ready - config: Optional[dict[str, Any]] = pipeline.get_child_pipeline_request() - - if config: - self.logger.child_pipeline_submitted(config["name"], pipeline.name) - # Convert dict to PipelineSetup for consistency - child_setup = PipelineSetup.from_dict(config) - self.new_pipeline_buffer.append(child_setup) - any_activity = True + try: + if self._telemetry_config: + self.telemetry = await self.flow.start_telemetry(**self._telemetry_config) + for fn in self._telemetry_subscribers: + self.telemetry.subscribe(fn) + + self.logger.manager_starting(len(pipeline_setups)) + + self.submit_new_pipelines(pipeline_setups) + + while True: + any_activity: bool = False + completed_pipelines: list[tuple] = [] + + for pipeline, pipeline_future in list(self.pipeline_tasks.items()): + # Check if pipeline needs adaptive step and isn't already running one + if ( + getattr(pipeline, "invoke_adaptive_step", False) + and pipeline not in self.adaptive_tasks + ): + adaptive_task: asyncio.Task = asyncio.create_task( + self._run_adaptive_fn(pipeline) + ) + self.adaptive_tasks[pipeline] = adaptive_task + any_activity = True + + # Check if pipeline has new config ready + config: Optional[dict[str, Any]] = pipeline.get_child_pipeline_request() + + if config: + self.logger.child_pipeline_submitted(config["name"], pipeline.name) + # Convert dict to PipelineSetup for consistency + child_setup = PipelineSetup.from_dict(config) + self.new_pipeline_buffer.append(child_setup) + any_activity = True + + # Check if parent should be killed + if getattr(pipeline, "kill_parent", False): + self.logger.pipeline_killed(pipeline.name) + pipeline_future.cancel() + completed_pipelines.append((pipeline, pipeline_future)) + continue - # Check if parent should be killed - if getattr(pipeline, "kill_parent", False): - self.logger.pipeline_killed(pipeline.name) - pipeline_future.cancel() - completed_pipelines.append((pipeline, pipeline_future)) - continue - - # Check if pipeline is done - but only mark as completed - # if adaptive task is also done - if pipeline_future.done(): - # If there's an adaptive task running, don't mark as completed yet + # Check if pipeline is done - but only mark as completed + # if adaptive task is also done + if pipeline_future.done(): + # If there's an adaptive task running, don't mark as completed yet + if pipeline in self.adaptive_tasks: + adaptive_task = self.adaptive_tasks[pipeline] + if not adaptive_task.done(): + continue + + completed_pipelines.append((pipeline, pipeline_future)) + + # Clean up completed pipelines - but only if their + # adaptive tasks are also done + actually_completed: list[ImpressBasePipeline] = [] + for pipeline, future in completed_pipelines: + # Double-check: only clean up if adaptive task is + # done or doesn't exist if pipeline in self.adaptive_tasks: adaptive_task = self.adaptive_tasks[pipeline] if not adaptive_task.done(): continue - - completed_pipelines.append((pipeline, pipeline_future)) - - # Clean up completed pipelines - but only if their - # adaptive tasks are also done - actually_completed: list[ImpressBasePipeline] = [] - for pipeline, future in completed_pipelines: - # Double-check: only clean up if adaptive task is - # done or doesn't exist - if pipeline in self.adaptive_tasks: - adaptive_task = self.adaptive_tasks[pipeline] - if not adaptive_task.done(): - continue - self.adaptive_tasks.pop(pipeline) - - self.pipeline_tasks.pop(pipeline, None) - exc = None - if future.done() and not future.cancelled(): - try: - exc = future.exception() - except Exception: - pass - if exc is not None: - self.logger.pipeline_failed(pipeline.name, exc) + self.adaptive_tasks.pop(pipeline) + + self.pipeline_tasks.pop(pipeline, None) + exc = None + if future.done() and not future.cancelled(): + try: + exc = future.exception() + except Exception: + pass + if exc is not None: + self.logger.pipeline_failed(pipeline.name, exc) + else: + self.logger.pipeline_completed(pipeline.name) + actually_completed.append(pipeline) + + completed_pipelines = actually_completed + + # Clean up completed adaptive tasks + completed_adaptive: list[ImpressBasePipeline] = [] + for pipeline, adaptive_task in list(self.adaptive_tasks.items()): + if adaptive_task.done(): + completed_adaptive.append(pipeline) + + for pipeline in completed_adaptive: + self.adaptive_tasks.pop(pipeline, None) + + # Submit new pipelines; capture count before clearing so + # activity_summary reports the real number submitted. + if self.new_pipeline_buffer: + buffered_count = len(self.new_pipeline_buffer) + self.submit_new_pipelines(self.new_pipeline_buffer) + self.new_pipeline_buffer.clear() + any_activity = True else: - self.logger.pipeline_completed(pipeline.name) - actually_completed.append(pipeline) - - completed_pipelines = actually_completed - - # Clean up completed adaptive tasks - completed_adaptive: list[ImpressBasePipeline] = [] - for pipeline, adaptive_task in list(self.adaptive_tasks.items()): - if adaptive_task.done(): - completed_adaptive.append(pipeline) - - for pipeline in completed_adaptive: - self.adaptive_tasks.pop(pipeline, None) - - # Submit new pipelines - if self.new_pipeline_buffer: - self.submit_new_pipelines(self.new_pipeline_buffer) - self.new_pipeline_buffer.clear() - any_activity = True - - # Log activity summary periodically - if any_activity: - self.logger.activity_summary( - len(self.pipeline_tasks), - len(self.adaptive_tasks), - len(self.new_pipeline_buffer), - ) - - # Exit condition - if ( - not self.pipeline_tasks - and not self.new_pipeline_buffer - and not self.adaptive_tasks - ): - self.logger.manager_exiting() - self.logger.separator("IMPRESS MANAGER FINISHED") - break - - if not any_activity: - await asyncio.sleep(0.5) + buffered_count = 0 + + # Log activity summary periodically + if any_activity: + self.logger.activity_summary( + len(self.pipeline_tasks), + len(self.adaptive_tasks), + buffered_count, + ) + + # Exit condition + if ( + not self.pipeline_tasks + and not self.new_pipeline_buffer + and not self.adaptive_tasks + ): + self.logger.manager_exiting() + self.logger.separator("IMPRESS MANAGER FINISHED") + break + + if not any_activity: + await asyncio.sleep(0.5) + finally: + await self.flow.shutdown() diff --git a/src/impress/pipelines/impress_pipeline.py b/src/impress/pipelines/impress_pipeline.py index f856fab..d2e70d5 100644 --- a/src/impress/pipelines/impress_pipeline.py +++ b/src/impress/pipelines/impress_pipeline.py @@ -95,7 +95,6 @@ async def get_scores_map(self): """Optional: Return scores mapping""" return {} - @abstractmethod async def finalize(self): """Optional: Cleanup or finalization logic""" pass diff --git a/src/impress/utils/logger.py b/src/impress/utils/logger.py index 200d2c9..117b0a7 100644 --- a/src/impress/utils/logger.py +++ b/src/impress/utils/logger.py @@ -34,10 +34,14 @@ class LogLevel(Enum): class ImpressLogger: - def __init__(self, name="ImpressManager", use_colors=True, output_stream=None): + _LEVEL_ORDER = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR, LogLevel.CRITICAL] + + def __init__(self, name="ImpressManager", use_colors=True, output_stream=None, + min_level: LogLevel = LogLevel.DEBUG): self.name = name self.use_colors = use_colors self.output_stream = output_stream or sys.stdout + self.min_level = min_level self.level_colors = { LogLevel.DEBUG: Colors.BRIGHT_BLACK, @@ -91,40 +95,42 @@ def _format_message(self, level, component, message, pipeline_name=None): f"{timestamp} {colored_level} {colored_component}{pipeline_part} {message}" ) - def _write_log(self, message, to_stderr=False): - stream = sys.stderr if to_stderr else self.output_stream - stream.write(message + "\n") - stream.flush() + def _is_enabled(self, level: LogLevel) -> bool: + return self._LEVEL_ORDER.index(level) >= self._LEVEL_ORDER.index(self.min_level) + + def _write_log(self, message): + self.output_stream.write(message + "\n") + self.output_stream.flush() def debug(self, message, component="manager", pipeline_name=None): - formatted = self._format_message( - LogLevel.DEBUG, component, message, pipeline_name - ) + if not self._is_enabled(LogLevel.DEBUG): + return + formatted = self._format_message(LogLevel.DEBUG, component, message, pipeline_name) self._write_log(formatted) def info(self, message, component="manager", pipeline_name=None): - formatted = self._format_message( - LogLevel.INFO, component, message, pipeline_name - ) + if not self._is_enabled(LogLevel.INFO): + return + formatted = self._format_message(LogLevel.INFO, component, message, pipeline_name) self._write_log(formatted) def warning(self, message, component="manager", pipeline_name=None): - formatted = self._format_message( - LogLevel.WARNING, component, message, pipeline_name - ) + if not self._is_enabled(LogLevel.WARNING): + return + formatted = self._format_message(LogLevel.WARNING, component, message, pipeline_name) self._write_log(formatted) def error(self, message, component="manager", pipeline_name=None): - formatted = self._format_message( - LogLevel.ERROR, component, message, pipeline_name - ) - self._write_log(formatted, to_stderr=True) + if not self._is_enabled(LogLevel.ERROR): + return + formatted = self._format_message(LogLevel.ERROR, component, message, pipeline_name) + self._write_log(formatted) def critical(self, message, component="manager", pipeline_name=None): - formatted = self._format_message( - LogLevel.CRITICAL, component, message, pipeline_name - ) - self._write_log(formatted, to_stderr=True) + if not self._is_enabled(LogLevel.CRITICAL): + return + formatted = self._format_message(LogLevel.CRITICAL, component, message, pipeline_name) + self._write_log(formatted) def pipeline_started(self, pipeline_name): colored_name = self._colorize(pipeline_name, Colors.BRIGHT_WHITE) @@ -187,10 +193,11 @@ def activity_summary(self, active_pipelines, active_adaptive, buffered_pipelines self.debug(summary, "manager") def pipeline_log(self, message, level=LogLevel.INFO): + if not self._is_enabled(level): + return pipeline_component = f"PIPELINE-{self.name.upper()}" formatted = self._format_message(level, pipeline_component, message) - stderr_levels = [LogLevel.ERROR, LogLevel.CRITICAL] - self._write_log(formatted, to_stderr=level in stderr_levels) + self._write_log(formatted) def separator(self, title=None): if title: From 9a95e35afab49f96c3474f56184efca4b7ca1cfb Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Wed, 2 Sep 2026 11:22:12 -0500 Subject: [PATCH 06/12] some updates based on CLAUDE suggestions --- examples/.gitignore | 14 + .../protein_binding/af2_multimer_reduced.sh | 41 --- examples/protein_binding/delta_env_setup.sh | 90 +++++- examples/protein_binding/delta_gpu_run.sh | 97 ++++-- examples/protein_binding/mpnn_wrapper.py | 2 +- .../protein_binding/plddt_extract_pipeline.py | 3 +- examples/protein_binding/protein_binding.py | 82 +++++- .../protein_binding/run_protein_binding.py | 50 +++- examples/protein_binding/scripts/s1_mpnn.sh | 4 +- .../protein_binding/scripts/s4_alphafold.sh | 32 +- examples/protein_binding/scripts/s4_boltz.sh | 60 +++- .../scripts/s5_plddt_extract.sh | 19 +- examples/small_molecule_binding/CLAUDE.md | 7 +- .../small_molecule_binding/delta_env_setup.sh | 252 ++++++++++++++++ .../small_molecule_binding/delta_gpu_run.sh | 148 ++++++++++ .../small_molecule_binding/pull_foundry.sh | 35 +++ .../small_molecule_binding/run_nonadaptive.py | 18 +- .../run_small_molecule_binding.py | 100 +++++-- .../small_molecule_binding/scripts/af2.sh | 3 +- .../scripts/mpnn_wrapper.sh | 28 -- .../small_molecule_binding/scripts/packmin.py | 9 - .../small_molecule_binding.py | 25 +- src/impress/__init__.py | 4 + src/impress/gpu.py | 25 ++ src/impress/impress_manager.py | 28 +- src/impress/pipelines/impress_pipeline.py | 10 +- src/impress/utils/logger.py | 28 +- tests/conftest.py | 5 +- tests/unit/test_logger.py | 278 ++++++++++++++++++ tests/unit/test_manager_core.py | 3 +- tests/unit/test_manager_life_cycle.py | 18 ++ .../unit/test_manager_pipeline_submission.py | 9 + tests/unit/test_pipeline_base.py | 157 ++++++++++ tests/unit/test_pipeline_setup.py | 142 +++++++++ 34 files changed, 1615 insertions(+), 211 deletions(-) create mode 100644 examples/.gitignore delete mode 100644 examples/protein_binding/af2_multimer_reduced.sh create mode 100755 examples/small_molecule_binding/delta_env_setup.sh create mode 100644 examples/small_molecule_binding/delta_gpu_run.sh create mode 100644 examples/small_molecule_binding/pull_foundry.sh delete mode 100755 examples/small_molecule_binding/scripts/mpnn_wrapper.sh create mode 100644 src/impress/gpu.py create mode 100644 tests/unit/test_logger.py create mode 100644 tests/unit/test_pipeline_base.py create mode 100644 tests/unit/test_pipeline_setup.py diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..31e9608 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,14 @@ +# SLURM log output (all workflows) +*/logs/ + +# Per-pipeline task directories written to cwd +*/p*/ + +# Dragon telemetry session dirs +*/telemetry/ + +# Run metadata written to cwd +*/runinfo + +# Legacy output dirs +*/myoutputs/ diff --git a/examples/protein_binding/af2_multimer_reduced.sh b/examples/protein_binding/af2_multimer_reduced.sh deleted file mode 100644 index 75e944b..0000000 --- a/examples/protein_binding/af2_multimer_reduced.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# have this *first* since they change the WORK env variable in jobs -# module reset -# module load cuda/12.3.0 - -set -e -set -x - -export XLA_PYTHON_CLIENT_PREALLOCATE="false" -export XLA_PYTHON_CLIENT_MEM_FRACTION=".75" -export XLA_PYTHON_CLIENT_ALLOCATOR="platform" - -#-B database.squashfs:/database:image-src=/ -INPUT_FASTA_FILE_DIR=$1 -INPUT_FASTA_FILE_NAME=$2 -OUTPUT_DATA_DIR=$3 - -: "${AF2_DATABASE:?AF2_DATABASE is not set (path to AlphaFold database dir)}" -: "${AF2_SIF:?AF2_SIF is not set (path to alphafold.sif container)}" - -apptainer run --nv --no-home \ - --bind $INPUT_FASTA_FILE_DIR:/fasta \ - --bind $OUTPUT_DATA_DIR:/dimer_models \ - --bind ${AF2_DATABASE}:/database \ - ${AF2_SIF} \ - --data_dir=/database \ - --uniref90_database_path=/database/uniref90/uniref90.fasta \ - --mgnify_database_path=/database/mgnify/mgy_clusters_2022_05.fa \ - --template_mmcif_dir=/database/pdb_mmcif/mmcif_files/ \ - --obsolete_pdbs_path=/database/pdb_mmcif/obsolete.dat \ - --fasta_paths=/fasta/$INPUT_FASTA_FILE_NAME \ - --output_dir=/dimer_models \ - --model_preset=multimer \ - --db_preset=reduced_dbs \ - --small_bfd_database_path=/database/small_bfd/bfd-first_non_consensus_sequences.fasta \ - --uniprot_database_path=/database/uniprot/uniprot.fasta \ - --pdb_seqres_database_path=/database/pdb_seqres/pdb_seqres.txt \ - --max_template_date=2020-12-01 \ - --use_gpu_relax=False \ - --num_multimer_predictions_per_model=1 \ No newline at end of file diff --git a/examples/protein_binding/delta_env_setup.sh b/examples/protein_binding/delta_env_setup.sh index 9abcad1..8caceab 100644 --- a/examples/protein_binding/delta_env_setup.sh +++ b/examples/protein_binding/delta_env_setup.sh @@ -129,9 +129,91 @@ echo "── Step 8: PyRosetta ──" "${PIP}" install -q pyrosetta-installer "${PY}" -c "import pyrosetta_installer; pyrosetta_installer.install_pyrosetta()" -# ── 9. Verify ──────────────────────────────────────────────────────────────── +# ── 9. Boltz (structure prediction — separate Python 3.12 conda env) ───────── echo "" -echo "── Step 9: Verifying installation ──" +echo "── Step 9: Boltz (separate conda env) ──" +# boltz 2.x requires numpy<2.0, scipy==1.13.1, etc. — none have Python 3.13 +# wheels, so boltz cannot be installed in the main Python 3.13 IMPRESS venv. +# Create a dedicated Python 3.12 conda env and install boltz there. +# s4_boltz.sh activates BOLTZ_VENV (set in delta_gpu_run.sh) instead of VIRTUAL_ENV. +MINIFORGE="${MINIFORGE:-/scratch/bblj/${USER}/miniforge3}" +BOLTZ_ENV="${BOLTZ_ENV:-${HOME}/ve/boltz}" +# Cache lives in home dir — scratch inode quota can't hold the 45K CCD files. +BOLTZ_CACHE="${BOLTZ_CACHE:-${HOME}/boltz}" +if [ ! -x "${BOLTZ_ENV}/bin/python" ]; then + echo " Creating conda env (Python 3.11) at ${BOLTZ_ENV}" + # Use /tmp for package cache to avoid scratch quota exhaustion. + CONDA_PKGS_DIRS=/tmp/conda_pkgs "${MINIFORGE}/bin/conda" create -p "${BOLTZ_ENV}" python=3.11 -y -q +else + echo " boltz conda env already exists at ${BOLTZ_ENV}" +fi +echo " Installing boltz into ${BOLTZ_ENV}" +"${BOLTZ_ENV}/bin/pip" install -q boltz +# Pre-warm the boltz cache so compute nodes (no internet) find weights ready. +# boltz downloads mols.tar (45K CCD pkl files) + model weights on first run. +# Running predict on the login node (which has internet) pre-populates them. +echo " Pre-warming boltz cache at ${BOLTZ_CACHE}" +mkdir -p "${BOLTZ_CACHE}" +_WARM_FA="$(mktemp /tmp/boltz_warmup_XXXXXX.fasta)" +printf ">warmup|A\nGSSGSSGSS\n>warmup|B\nGSSGSSGSS\n" > "${_WARM_FA}" +BOLTZ_CACHE_DIR="${BOLTZ_CACHE}" "${BOLTZ_ENV}/bin/boltz" predict "${_WARM_FA}" \ + --out_dir "$(mktemp -d /tmp/boltz_warmup_out_XXXXXX)" \ + --cache "${BOLTZ_CACHE}" \ + --override 2>&1 | grep -E "Download|Extracting|Error|error" || true +rm -f "${_WARM_FA}" +echo " Boltz cache pre-warm done (model weights cached at ${BOLTZ_CACHE})" + +# Pre-compute MSAs for all input proteins using the MSA server (login node has internet). +# protein_binding.py s3() reads from BOLTZ_MSA_CACHE and embeds paths in the FASTA so +# compute nodes do not need internet access. Entity 0 = receptor, entity 1 = peptide. +BOLTZ_MSA_CACHE="${BOLTZ_CACHE}/msa_cache" +mkdir -p "${BOLTZ_MSA_CACHE}" +echo " Pre-computing MSAs into ${BOLTZ_MSA_CACHE}" +# IMPRESS_BASE_DIR = parent of prod_in/; IMPRESS_OUTPUT_DIR = parent of af_pipeline_outputs_multi/ +_scratch="${SCRATCH:-/scratch/bblj/${USER}}" +_base_dir="${IMPRESS_BASE_DIR:-${_scratch}/IMPRESS_inputs}" +_out_dir="${IMPRESS_OUTPUT_DIR:-${_scratch}/IMPRESS_outputs}" +_msa_inputs_dir="${_base_dir}/prod_in" +if [ -d "${_msa_inputs_dir}" ]; then + for _pdb_dir in "${_msa_inputs_dir}"/p*_in; do + for _pdb in "${_pdb_dir}"/*.pdb; do + [ -f "${_pdb}" ] || continue + _stem="$(basename "${_pdb}" .pdb)" + _msa_csv="${BOLTZ_MSA_CACHE}/boltz_results_${_stem}/msa/${_stem}_0.csv" + if [ -f "${_msa_csv}" ]; then + echo " ${_stem}: MSA already cached, skipping" + continue + fi + echo " ${_stem}: generating MSA via server..." + _tmp_fa="$(mktemp /tmp/boltz_msa_XXXXXX.fasta)" + # Use FASTA from a prior run (sequences match the actual protein); fall back to + # a placeholder that will trigger MSA generation but gives a generic MSA. + _prior_fa="" + for _cand in "${_out_dir}"/af_pipeline_outputs_multi/*/af/fasta/"${_stem}.fa"; do + [ -f "${_cand}" ] && { _prior_fa="${_cand}"; break; } + done + if [ -n "${_prior_fa}" ]; then + cp "${_prior_fa}" "${_tmp_fa}" + else + printf ">pdz|protein\nGSSGSS\n>pep|protein\nGSSG\n" > "${_tmp_fa}" + fi + BOLTZ_CACHE_DIR="${BOLTZ_CACHE}" "${BOLTZ_ENV}/bin/boltz" predict "${_tmp_fa}" \ + --out_dir "${BOLTZ_MSA_CACHE}" \ + --use_msa_server \ + --cache "${BOLTZ_CACHE}" \ + --output_format pdb \ + --override 2>&1 | grep -E "MSA|Generat|Error|error|skip" || true + rm -f "${_tmp_fa}" + done + done +else + echo " prod_in not found at ${_msa_inputs_dir}, skipping MSA pre-compute" +fi +echo " MSA pre-compute done" + +# ── 10. Verify ─────────────────────────────────────────────────────────────── +echo "" +echo "── Step 10: Verifying installation ──" _check() { local label="$1"; shift if out=$("$@" 2>&1); then @@ -147,6 +229,8 @@ _check "rhapsody-py" "${PY}" -c "import rhapsody; print('ok')" _check "impress" "${PY}" -c "import impress; print('ok')" _check "torch" "${PY}" -c "import torch; print(torch.__version__)" _check "pandas" "${PY}" -c "import pandas; print(pandas.__version__)" +BOLTZ_ENV="${BOLTZ_ENV:-${HOME}/ve/boltz}" +_check "boltz" "${BOLTZ_ENV}/bin/python" -c "import boltz; print('ok')" echo "" echo "=================================================================" @@ -158,6 +242,6 @@ echo "" echo "Run the pipeline:" echo " export SCRATCH=${SCRATCH}" echo " export SBATCH_ACCOUNT=bblj-delta-gpu" -echo " cd ${IMPRESS_DIR}/examples/protien_binding_usecase" +echo " cd ${IMPRESS_DIR}/examples/protein_binding" echo " sbatch delta_gpu_run.sh" echo "=================================================================" diff --git a/examples/protein_binding/delta_gpu_run.sh b/examples/protein_binding/delta_gpu_run.sh index 9ccd267..7d333b0 100644 --- a/examples/protein_binding/delta_gpu_run.sh +++ b/examples/protein_binding/delta_gpu_run.sh @@ -1,43 +1,100 @@ #!/bin/bash +# +# Protein Binding Pipeline — SLURM batch script (Delta HPC / GPU) +# +# Set before calling sbatch (only SBATCH_ACCOUNT and SCRATCH are required; +# the rest default to standard Delta locations): +# export SBATCH_ACCOUNT=-delta-gpu +# export SCRATCH=/scratch/ +# +# Example: +# sbatch delta_gpu_run.sh +# +# Account: set SBATCH_ACCOUNT=-delta-gpu before calling sbatch #SBATCH --partition=gpuA40x4 #SBATCH --nodes=1 #SBATCH --tasks-per-node=1 #SBATCH --cpus-per-task=16 -#SBATCH --gpus=4 -#SBATCH --exclusive -#SBATCH --time=00:30:00 +#SBATCH --gpus-per-node=4 +#SBATCH --mem=220G +#SBATCH --time=02:00:00 #SBATCH --job-name=impress_protein -#SBATCH --mail-user=mariya.goliyad@rutgers.edu +#SBATCH --mail-user=mg2347@soe.rutgers.edu #SBATCH --mail-type=END,FAIL #SBATCH --output=logs/impress_%j.out #SBATCH --error=logs/impress_%j.err +# NOTE: logs/ must exist before sbatch is called. Create it once with: +# mkdir -p /logs set -e +# ── Sanity checks ───────────────────────────────────────────────────────────── +if [ -z "${SBATCH_ACCOUNT:-}${SLURM_JOB_ACCOUNT:-}" ]; then + echo "WARNING: SBATCH_ACCOUNT is not set — job may be charged to default account." +fi +echo "Account: ${SLURM_JOB_ACCOUNT:-unknown}" + +if [ -z "${SCRATCH:-}" ]; then + echo "ERROR: SCRATCH is not set." + echo " export SCRATCH=/scratch/ && sbatch delta_gpu_run.sh" + exit 1 +fi + # ── System library paths (Delta-specific, required by Dragon) ───────────────── export CUDA_HOME=/opt/nvidia/hpc_sdk/Linux_x86_64/25.3/cuda/12.8 export MPI_LIB=/opt/cray/pe/mpich/8.1.32/ofi/gnu/11.2/lib-abi-mpich export FAB_LIB=/opt/cray/libfabric/1.22.0/lib64 export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH:-} -# ── Paths — edit these when moving to a different system ───────────────────── -: "${SCRATCH:?SCRATCH is not set (e.g. export SCRATCH=/scratch/bblj)}" -export MPNN_PATH="$SCRATCH/$USER/ProteinMPNN" -IMPRESS_VENV="${IMPRESS_VENV:-$HOME/ve/impress}" -export AF2_DATABASE="/scratch/rhaas/SUP-5301/database" -export AF2_SIF="/scratch/rhaas/SUP-5301/alphafold.sif" -export IMPRESS_INPUT_DIR="$SCRATCH/$USER/IMPRESS_inputs/prod_in" -export IMPRESS_OUTPUT_DIR="$SCRATCH/$USER/IMPRESS_outputs" -export IMPRESS_SCRIPTS_DIR="$SCRATCH/$USER/IMPRESS/examples/protein_binding" +# ── Environment ─────────────────────────────────────────────────────────────── +IMPRESS_VENV="${IMPRESS_VENV:-${HOME}/ve/impress}" +unset SLURM_EXPORT_ENV +source "${IMPRESS_VENV}/bin/activate" +dragon-config add --ofi-runtime-lib="${FAB_LIB}" + +# ── Tool paths (adjust for your allocation) ─────────────────────────────────── +export MPNN_PATH="${MPNN_PATH:-${SCRATCH}/${USER}/ProteinMPNN}" +export AF2_DATABASE="${AF2_DATABASE:-${SCRATCH}/${USER}/alphafold_database}" +export AF2_SIF="${AF2_SIF:-${SCRATCH}/${USER}/alphafold.sif}" +# Boltz lives in a separate Python 3.12 conda env (boltz 2.x requires numpy<2.0, +# scipy==1.13.1 etc. which have no Python 3.13 wheels). +export BOLTZ_VENV="${BOLTZ_VENV:-${HOME}/ve/boltz}" +# Boltz model weight cache — kept in home dir; scratch inode quota can't hold +# the 45K CCD molecule files that boltz extracts from mols.tar on first run. +export BOLTZ_CACHE_DIR="${BOLTZ_CACHE_DIR:-${HOME}/boltz}" +mkdir -p "${BOLTZ_CACHE_DIR}" + +# ── IMPRESS paths ───────────────────────────────────────────────────────────── +export IMPRESS_SCRIPTS_DIR="${IMPRESS_SCRIPTS_DIR:-${SCRATCH}/${USER}/IMPRESS/examples/protein_binding}" +# IMPRESS_BASE_DIR: parent of prod_in/ — pipeline builds prod_in/_in from here +export IMPRESS_BASE_DIR="${IMPRESS_BASE_DIR:-${SCRATCH}/${USER}/IMPRESS_inputs}" +export IMPRESS_OUTPUT_DIR="${IMPRESS_OUTPUT_DIR:-${SCRATCH}/${USER}/IMPRESS_outputs}" + +# IMPRESS_TEST_MODE=1: 2 pipelines, max_passes=1, no child pipelines. +# Runs a single MPNN → score → AF2 cycle to verify end-to-end path. +# Set before sbatch: IMPRESS_TEST_MODE=1 sbatch delta_gpu_run.sh +export IMPRESS_TEST_MODE="${IMPRESS_TEST_MODE:-0}" +echo "TEST_MODE: ${IMPRESS_TEST_MODE}" # ── Working directory ───────────────────────────────────────────────────────── -WORKDIR="$SCRATCH/$USER/IMPRESS/examples/protein_binding" -cd "$WORKDIR" +WORKDIR="${IMPRESS_SCRIPTS_DIR}" +cd "${WORKDIR}" mkdir -p logs -source "${IMPRESS_VENV}/bin/activate" -dragon-config add --ofi-runtime-lib="${FAB_LIB}" - # ── Run ─────────────────────────────────────────────────────────────────────── -rm -rf asyncflow.session.* -dragon -s run_protein_binding.py +# asyncflow session dirs now go to /tmp (node-local, no quota) via +# IMPRESS_SESSION_DIR; no need to clean them from cwd. + +# -s = single-node Dragon runtime; -m = multi-node (uses MPI/OFI fabric). +if [ "${SLURM_NNODES:-1}" -gt 1 ]; then + DRAGON_MODE="-m" +else + DRAGON_MODE="-s" +fi + +rm -f ddict_orc* + +echo "Running: dragon ${DRAGON_MODE} run_protein_binding.py (nodes=${SLURM_NNODES:-1})" +dragon ${DRAGON_MODE} run_protein_binding.py + +echo "=== Protein Binding pipeline done: $(date) ===" diff --git a/examples/protein_binding/mpnn_wrapper.py b/examples/protein_binding/mpnn_wrapper.py index cf3c475..811fd4d 100644 --- a/examples/protein_binding/mpnn_wrapper.py +++ b/examples/protein_binding/mpnn_wrapper.py @@ -32,7 +32,7 @@ index=args.index fix=args.fix #default false, specify non fixed seqs=args.seqs -if seqs == None: +if seqs is None: seqs=1 #default 1 design per structure tie=args.tie homo=args.homo diff --git a/examples/protein_binding/plddt_extract_pipeline.py b/examples/protein_binding/plddt_extract_pipeline.py index d1c8a7d..6dc57bd 100644 --- a/examples/protein_binding/plddt_extract_pipeline.py +++ b/examples/protein_binding/plddt_extract_pipeline.py @@ -59,4 +59,5 @@ print(f"Processed {len(rows)} structure(s)") df = pd.DataFrame(rows, columns=['ID', 'avg_plddt', 'ptm', 'avg_pae']) -df.to_csv('af_stats_' + args.out + '_pass_' + args.iter + '.csv', index=False) +csv_path = os.path.join(args.path, 'af_stats_' + args.out + '_pass_' + args.iter + '.csv') +df.to_csv(csv_path, index=False) diff --git a/examples/protein_binding/protein_binding.py b/examples/protein_binding/protein_binding.py index 15bde0d..52f80e2 100644 --- a/examples/protein_binding/protein_binding.py +++ b/examples/protein_binding/protein_binding.py @@ -37,6 +37,8 @@ def __init__(self, name, flow, configs=None, **kwargs): self.mpnn_path = kwargs.get("mpnn_path") or MPNN_PATH if not self.mpnn_path: raise ValueError("mpnn_path must be supplied via kwarg or MPNN_PATH env var") + self.peptide_seq: str = kwargs.get("peptide_seq", "EGYQDYEPEA") + self.policy = kwargs.get("policy", None) # Sequence and score state self.current_scores = {} @@ -48,12 +50,18 @@ def __init__(self, name, flow, configs=None, **kwargs): # Input-related self.fasta_list_2 = kwargs.get("fasta_list_2", []) self.base_path = kwargs.get("base_path", os.getcwd()) + # input_base_path is the parent of prod_in/; defaults to base_path so + # existing callers that keep input data next to scripts still work. + self.input_base_path = kwargs.get("input_base_path", self.base_path) + # output_base_path is where af_pipeline_outputs_multi/ is written; + # defaults to base_path so existing callers without IMPRESS_OUTPUT_DIR work. + self.output_base_path = kwargs.get("output_base_path", self.base_path) self.scripts_path = os.path.join(self.base_path, "scripts") - self.input_path = os.path.join(self.base_path, f"prod_in/{self.name}_in") + self.input_path = os.path.join(self.input_base_path, f"prod_in/{self.name}_in") # Output paths self.output_path = os.path.join( - self.base_path, "af_pipeline_outputs_multi", self.name + self.output_base_path, "af_pipeline_outputs_multi", self.name ) self.output_path_mpnn = os.path.join(self.output_path, "mpnn") self.output_path_af = os.path.join( @@ -67,12 +75,9 @@ def __init__(self, name, flow, configs=None, **kwargs): def set_up_new_pipeline_dirs(self, new_pipeline_name): base_output = os.path.join( - self.base_path, "af_pipeline_outputs_multi", new_pipeline_name + self.output_base_path, "af_pipeline_outputs_multi", new_pipeline_name ) - input_dir = os.path.join(self.base_path, f"prod_in/{new_pipeline_name}_in") - - if os.path.isdir(base_output): - return # already exists, nothing to do + input_dir = os.path.join(self.input_base_path, f"prod_in/{new_pipeline_name}_in") # all directories to create subdirs = [ @@ -96,16 +101,17 @@ def set_up_new_pipeline_dirs(self, new_pipeline_name): def register_pipeline_tasks(self): """Register all pipeline tasks""" - @self.auto_register_task(capture_stdio=True) # MPNN - async def s1(task_description={"gpus_per_rank": 1}): + @self.auto_register_task(local_task=True) # MPNN + async def s1(): self.step_id += 1 mpnn_script = os.path.join(self.base_path, "mpnn_wrapper.py") output_dir = os.path.join(self.output_path_mpnn, f"job_{self.passes}") + os.makedirs(output_dir, exist_ok=True) chain = "A" input_path = self.input_path if self.passes == 1 else self.output_path_af - return ( + cmd = ( f"bash {self.scripts_path}/s1_mpnn.sh " f"{mpnn_script} " f"{input_path} " @@ -114,6 +120,15 @@ async def s1(task_description={"gpus_per_rank": 1}): f"{self.num_seqs} " f"{chain}" ) + log_path = os.path.join(output_dir, "mpnn_run.log") + with open(log_path, "w") as lf: + proc = await asyncio.create_subprocess_shell( + cmd, stdout=lf, stderr=asyncio.subprocess.STDOUT, + env=self._gpu_env(), + ) + rc = await proc.wait() + if rc != 0: + raise RuntimeError(f"s1 MPNN failed (exit {rc})") @self.auto_register_task(local_task=True) async def s2(): @@ -141,17 +156,38 @@ async def s2(): async def s3(): self.step_id += 1 output_dir = os.path.join(self.output_path, "af", "fasta") + # Pre-computed MSA cache (populated by delta_env_setup.sh on the login node, + # which has internet access). Entity 0 = receptor (PDZ), entity 1 = peptide. + msa_cache = os.path.expanduser("~/boltz/msa_cache") fasta_file_to_return = [] for fasta_file in self.fasta_list_2: base_name = fasta_file.split(".")[0] fasta_file_to_return.append(base_name) design_seq = self.iter_seqs[base_name][self.seq_rank][0] - pep_seq = "EGYQDYEPEA" + pep_seq = self.peptide_seq + + # Boltz embeds pre-computed MSAs in the FASTA header rather than + # running MSA search at prediction time (compute nodes have no internet). + # Naming convention: entity 0 = receptor (PDZ), entity 1 = peptide. + pdz_msa = os.path.join( + msa_cache, f"boltz_results_{base_name}", "msa", f"{base_name}_0.csv" + ) + pep_msa = os.path.join( + msa_cache, f"boltz_results_{base_name}", "msa", f"{base_name}_1.csv" + ) + if os.path.exists(pdz_msa) and os.path.exists(pep_msa): + # |path/to/msa.csv| tells Boltz to load the pre-computed MSA + pdz_tag = f">pdz|protein|{pdz_msa}" + pep_tag = f">pep|protein|{pep_msa}" + else: + # |empty| runs single-sequence mode — no MSA, slightly less accurate + pdz_tag = ">pdz|protein|empty" + pep_tag = ">pep|protein|empty" fasta_path = os.path.join(output_dir, f"{base_name}.fa") with open(fasta_path, "w") as f: - f.write(f">pdz|protein\n{design_seq}\n>pep|protein\n{pep_seq}\n") + f.write(f"{pdz_tag}\n{design_seq}\n{pep_tag}\n{pep_seq}\n") return fasta_file_to_return @@ -164,8 +200,8 @@ async def s3(): # f"{self.output_path}/af/prediction/dimer_models/{target_fasta}" # ) - @self.auto_register_task(capture_stdio=True) - async def s4(target_fasta, task_description={"gpus_per_rank": 1}): # noqa: B006 + @self.auto_register_task(local_task=True) + async def s4(target_fasta): self.step_id += 1 cmd = ( f"bash {self.scripts_path}/s4_boltz.sh " @@ -173,7 +209,16 @@ async def s4(target_fasta, task_description={"gpus_per_rank": 1}): # noqa: B006 f"{self.output_path}/af/prediction/dimer_models/{target_fasta}" ) self.logger.pipeline_log(f"s4 command for {target_fasta}: {cmd}") - return cmd + # s4_boltz.sh tees its own output to boltz_run.log in the output dir + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + env=self._gpu_env(), + ) + rc = await proc.wait() + if rc != 0: + raise RuntimeError(f"Boltz failed for {target_fasta} (exit {rc})") @self.auto_register_task(local_task=True) async def s4_post_exec( @@ -208,7 +253,7 @@ async def s5(): self.step_id += 1 return ( f"bash {self.scripts_path}/s5_plddt_extract.sh " - f"{self.base_path} " + f"{self.output_base_path} " f"{self.passes} " f"{self.name}" ) @@ -316,11 +361,16 @@ async def _guarded_s4(target_fasta): self.logger.pipeline_log(f"s4 DONE for {fasta_name}") s4_post_results = await asyncio.gather(*post_exec_tasks, return_exceptions=True) + any_s4_ok = False for fasta_name, result in zip(fasta_files, s4_post_results): if isinstance(result, Exception): self.logger.pipeline_log(f"s4_post_exec FAILED for {fasta_name}: {result}") else: self.logger.pipeline_log(f"s4_post_exec DONE for {fasta_name}") + any_s4_ok = True + + if not any_s4_ok: + raise RuntimeError("All s4 tasks failed — skipping pLDDT extraction") self.logger.pipeline_log("Submitting pLDTT extraction task") diff --git a/examples/protein_binding/run_protein_binding.py b/examples/protein_binding/run_protein_binding.py index b006f5c..be9fb47 100644 --- a/examples/protein_binding/run_protein_binding.py +++ b/examples/protein_binding/run_protein_binding.py @@ -8,14 +8,22 @@ from rhapsody.telemetry import define_event from rhapsody.telemetry.events import make_event -from impress import PipelineSetup -from impress import ImpressManager +from impress import GPUPolicy, _find_gpus, _make_policy, ImpressManager, PipelineSetup from protein_binding import ProteinBindingPipeline import rhapsody, logging rhapsody.enable_logging(level=logging.DEBUG) +# ── Test mode (IMPRESS_TEST_MODE=1) ─────────────────────────────────────── +# Runs 2 pipelines with max_passes=1 and no child pipelines, so a single +# MPNN → score → AF2 cycle completes for integration testing. +TEST_MODE = os.getenv("IMPRESS_TEST_MODE", "0") == "1" +N_PIPELINES = 2 if TEST_MODE else 16 +MAX_PASSES = 1 if TEST_MODE else 10 +MAX_SUB_PIPELINES_OVERRIDE = 0 if TEST_MODE else None # None = use inline default + + # --------------------------------------------------------------------------- # Custom application-level telemetry events # --------------------------------------------------------------------------- @@ -81,12 +89,12 @@ async def impress_protein_bind() -> None: # adaptive_decision closes over `manager` so it can emit events via # manager.telemetry, which is set inside start() before any pipeline runs. async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[str, Any]]: - MAX_SUB_PIPELINES: int = 3 + MAX_SUB_PIPELINES: int = MAX_SUB_PIPELINES_OVERRIDE if MAX_SUB_PIPELINES_OVERRIDE is not None else 3 tel = manager.telemetry sid = tel.session_id if tel else None # Read current scores from CSV - file_name = os.path.join(pipeline.base_path, f'af_stats_{pipeline.name}_pass_{pipeline.passes}.csv') + file_name = os.path.join(pipeline.output_base_path, f'af_stats_{pipeline.name}_pass_{pipeline.passes}.csv') with open(file_name) as fd: for line in fd.readlines()[1:]: line = line.strip() @@ -109,13 +117,14 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[s continue prev_score = pipeline.previous_scores[protein] - decision = await adaptive_criteria(curr_score, prev_score) + decision = adaptive_criteria(curr_score, prev_score) pipeline.logger.pipeline_log(f'Adaptive decision: {decision}') if tel: tel.emit(make_event( ProteinScore, session_id=sid, + backend="rhapsody", protein=protein, pipeline_name=pipeline.name, pass_num=pipeline.passes, @@ -136,13 +145,14 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[s for protein in sub_iter_seqs: src = f'{pipeline.output_path_af}/{protein}.pdb' - dst = f'{pipeline.base_path}/prod_in/{new_name}_in/{protein}.pdb' + dst = f'{pipeline.input_base_path}/prod_in/{new_name}_in/{protein}.pdb' shutil.copyfile(src, dst) if tel: tel.emit(make_event( ChildPipelineSpawned, session_id=sid, + backend="rhapsody", parent_name=pipeline.name, child_name=new_name, num_proteins=len(sub_iter_seqs), @@ -161,6 +171,8 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[s 'seq_rank': pipeline.seq_rank + 1, 'sub_order': pipeline.sub_order + 1, 'previous_scores': copy.deepcopy(pipeline.previous_scores), + 'input_base_path': pipeline.input_base_path, + 'output_base_path': pipeline.output_base_path, } } @@ -178,6 +190,7 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[s tel.emit(make_event( PassSummary, session_id=sid, + backend="rhapsody", pipeline_name=pipeline.name, pass_num=pipeline.passes, num_proteins=len(pipeline.current_scores), @@ -185,13 +198,32 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[s child_spawned=child_spawned, )) + # IMPRESS_SCRIPTS_DIR = protein_binding examples dir (scripts/, mpnn_wrapper.py) + # IMPRESS_BASE_DIR = parent of prod_in/ (input PDB files) + # IMPRESS_OUTPUT_DIR = where af_pipeline_outputs_multi/ is written + scripts_dir = os.environ.get( + "IMPRESS_SCRIPTS_DIR", os.path.dirname(os.path.abspath(__file__)) + ) + input_base_dir = os.environ.get("IMPRESS_BASE_DIR", scripts_dir) + output_base_dir = os.environ.get("IMPRESS_OUTPUT_DIR", scripts_dir) + os.makedirs(output_base_dir, exist_ok=True) + + all_gpus = _find_gpus() + pipeline_setups: List[PipelineSetup] = [ PipelineSetup( name=f"p{str(i)}", type=ProteinBindingPipeline, - adaptive_fn=adaptive_decision + config={ + "base_path": scripts_dir, + "input_base_path": input_base_dir, + "output_base_path": output_base_dir, + "policy": _make_policy(all_gpus, i - 1), + }, + adaptive_fn=adaptive_decision, + max_passes=MAX_PASSES, ) - for i in range(1, 17) + for i in range(1, N_PIPELINES + 1) ] await manager.start(pipeline_setups=pipeline_setups) @@ -202,8 +234,8 @@ async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[s dur = summary.get("duration") if dur: print(f"[TELEMETRY] mean task time: {dur['mean_seconds'] * 1000:.1f} ms") + await manager.telemetry.stop() - await manager.flow.shutdown() if __name__ == "__main__": diff --git a/examples/protein_binding/scripts/s1_mpnn.sh b/examples/protein_binding/scripts/s1_mpnn.sh index 67f4110..b490f10 100755 --- a/examples/protein_binding/scripts/s1_mpnn.sh +++ b/examples/protein_binding/scripts/s1_mpnn.sh @@ -11,8 +11,8 @@ mpnn_path="$4" num_seqs="$5" chain="$6" -source /anvil/projects/x-nairr240405/mason/LigandMPNN/.venv/bin/activate -#source /ocean/projects/dmr170002p/hooten/LigandMPNN/.venv/bin/activate +# Re-activate the IMPRESS venv inside Dragon tasks (VIRTUAL_ENV is exported by sbatch). +[ -n "${VIRTUAL_ENV:-}" ] && source "${VIRTUAL_ENV}/bin/activate" python3 "$mpnn_script" \ -pdb="$input_path" \ diff --git a/examples/protein_binding/scripts/s4_alphafold.sh b/examples/protein_binding/scripts/s4_alphafold.sh index 29b2126..d72a7a5 100755 --- a/examples/protein_binding/scripts/s4_alphafold.sh +++ b/examples/protein_binding/scripts/s4_alphafold.sh @@ -7,14 +7,32 @@ set -euo pipefail fasta_path="$1" output_dir="$2" -module load modtree/gpu -module load cuda/12.8.0 -module load gcc/11.2.0 -#source /anvil/scratch/x-mason/IMPRESS/.venv/bin/activate -source /ocean/projects/dmr170002p/hooten/IMPRESS/.venv/bin/activate +# Re-activate the IMPRESS venv inside Dragon tasks (VIRTUAL_ENV is exported by sbatch). +[ -n "${VIRTUAL_ENV:-}" ] && source "${VIRTUAL_ENV}/bin/activate" -pixi run --manifest-path /anvil/scratch/x-mason/localcolabfold \ - colabfold_batch \ +# ── Test-mode stub (IMPRESS_TEST_MODE=1) ────────────────────────────────── +# Write minimal Boltz-shaped output so plddt_extract_pipeline can run without +# invoking the real AlphaFold model. +if [ "${IMPRESS_TEST_MODE:-0}" = "1" ]; then + name="$(basename "${fasta_path}" .fa)" + pred_dir="${output_dir}/boltz_results_${name}/predictions/${name}" + mkdir -p "${pred_dir}" + python3 - "${pred_dir}" "${name}" <<'PYEOF' +import sys, json +import numpy as np +pred_dir, name = sys.argv[1], sys.argv[2] +n = 110 # 100 PDZ residues + 10 peptide (PEP_LEN=10 assumed by extractor) +np.savez(f"{pred_dir}/plddt_{name}_model_0.npz", plddt=np.full(n, 0.85)) +np.savez(f"{pred_dir}/pae_{name}_model_0.npz", pae=np.full((n, n), 2.0)) +with open(f"{pred_dir}/confidence_{name}_model_0.json", "w") as f: + json.dump({"iptm": 0.75, "ptm": 0.80}, f) +PYEOF + echo "[MOCK] s4_alphafold stub done for ${name}" + exit 0 +fi +# ── End test-mode stub ──────────────────────────────────────────────────── + +colabfold_batch \ --model-type alphafold2_multimer_v3 \ --max-template-date 2020-12-01 \ --rank multimer \ diff --git a/examples/protein_binding/scripts/s4_boltz.sh b/examples/protein_binding/scripts/s4_boltz.sh index bab328c..a10cef9 100755 --- a/examples/protein_binding/scripts/s4_boltz.sh +++ b/examples/protein_binding/scripts/s4_boltz.sh @@ -7,25 +7,59 @@ set -e fasta_path="$1" output_dir="$2" -#source /ocean/projects/dmr170002p/hooten/IMPRESS/.venv/bin/activate -source /anvil/projects/x-nairr240405/mason/IMPRESS/.venv/bin/activate -module load modtree/gpu +# Boltz requires Python <=3.12 (numpy<2.0 etc.) so it lives in its own env. +# BOLTZ_VENV may point to a conda env (no bin/activate) or a pip venv; prepend +# its bin/ to PATH so the correct python/boltz are found in either case. +_BOLTZ_ENV="${BOLTZ_VENV:-${VIRTUAL_ENV:-}}" +[ -n "${_BOLTZ_ENV}" ] && export PATH="${_BOLTZ_ENV}/bin:${PATH}" export SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt -# Boltz caches MSA in boltz_results_/msa/ and reuses it across runs even with -# --override, so pass 2+ would fold new MPNN-designed sequences using the pass-1 MSA. -# Delete the stale MSA before each run to force recomputation for the current sequence. -fasta_stem=$(basename "${fasta_path}" .fa) -stale_msa="${output_dir}/boltz_results_${fasta_stem}/msa" -if [ -d "${stale_msa}" ]; then - rm -rf "${stale_msa}" +# Compute nodes typically have no internet access, so --use_msa_server is off +# by default. Set BOLTZ_USE_MSA_SERVER=1 to enable it on nodes with internet. +_MSA_FLAG="" +[ "${BOLTZ_USE_MSA_SERVER:-0}" = "1" ] && _MSA_FLAG="--use_msa_server" + +# ── Test-mode stub (IMPRESS_TEST_MODE=1) ────────────────────────────────── +# Write minimal Boltz-shaped output so downstream steps (plddt_extract_pipeline) +# can run without invoking the real Boltz model. numpy is available because +# BOLTZ_VENV/bin is already on PATH above. +if [ "${IMPRESS_TEST_MODE:-0}" = "1" ]; then + name="$(basename "${fasta_path}" .fa)" + pred_dir="${output_dir}/boltz_results_${name}/predictions/${name}" + mkdir -p "${pred_dir}" + python3 - "${pred_dir}" "${name}" <<'PYEOF' +import sys, json +import numpy as np +pred_dir, name = sys.argv[1], sys.argv[2] +n = 110 # 100 PDZ residues + 10 peptide (PEP_LEN=10 assumed by extractor) +np.savez(f"{pred_dir}/plddt_{name}_model_0.npz", plddt=np.full(n, 0.85)) +np.savez(f"{pred_dir}/pae_{name}_model_0.npz", pae=np.full((n, n), 2.0)) +with open(f"{pred_dir}/confidence_{name}_model_0.json", "w") as f: + json.dump({"iptm": 0.75, "ptm": 0.80}, f) +PYEOF + echo "[MOCK] s4_boltz stub done for ${name}" + exit 0 fi +# ── End test-mode stub ──────────────────────────────────────────────────── + +mkdir -p "${output_dir}" + +# Prevent PyTorch Lightning from installing SLURM auto-requeue signal handlers. +# PL detects SLURM_JOB_ID and registers SIGTERM/SIGUSR handlers that keep the +# process group alive after Boltz finishes, causing Dragon to report +# "ProcessGroup manager is not in state State.DEAD". +unset SLURM_JOB_ID boltz predict \ "${fasta_path}" \ --out_dir "${output_dir}" \ - --use_msa_server \ - --cache /anvil/projects/x-nairr240405/mason/boltz \ + ${_MSA_FLAG} \ + --cache "${BOLTZ_CACHE_DIR:-${HOME}/.boltz}" \ --output_format pdb \ --write_full_pae \ - --override + --no_kernels \ + --devices 1 \ + --override \ + 2>&1 | tee "${output_dir}/boltz_run.log" +# tee exits 0; check the actual boltz exit code via PIPESTATUS +test "${PIPESTATUS[0]}" -eq 0 diff --git a/examples/protein_binding/scripts/s5_plddt_extract.sh b/examples/protein_binding/scripts/s5_plddt_extract.sh index 38cb576..11a3367 100755 --- a/examples/protein_binding/scripts/s5_plddt_extract.sh +++ b/examples/protein_binding/scripts/s5_plddt_extract.sh @@ -2,16 +2,19 @@ set -e # Step 5: pLDDT extraction -# Args: $1=base_path $2=iter $3=out_name +# Args: $1=output_base_path $2=iter $3=out_name -base_path="$1" +output_base_path="$1" iter="$2" out_name="$3" -source /anvil/projects/x-nairr240405/mason/IMPRESS/.venv/bin/activate -#source /ocean/projects/dmr170002p/hooten/IMPRESS/.venv/bin/activate +# plddt_extract_pipeline.py lives one level above this scripts/ directory. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -python3 "$base_path/plddt_extract_pipeline.py" \ - --path="$base_path" \ - --iter="$iter" \ - --out="$out_name" +# Re-activate the IMPRESS venv inside Dragon tasks (VIRTUAL_ENV is exported by sbatch). +[ -n "${VIRTUAL_ENV:-}" ] && source "${VIRTUAL_ENV}/bin/activate" + +python3 "${SCRIPT_DIR}/../plddt_extract_pipeline.py" \ + --path="${output_base_path}" \ + --iter="${iter}" \ + --out="${out_name}" diff --git a/examples/small_molecule_binding/CLAUDE.md b/examples/small_molecule_binding/CLAUDE.md index aaa9f96..2e51df2 100644 --- a/examples/small_molecule_binding/CLAUDE.md +++ b/examples/small_molecule_binding/CLAUDE.md @@ -7,6 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | Date | Commit | Notes | |---|---|---| | 2026-04-06 | 3390b61 | change log added | +| 2026-09-01 | — | RunConfig dataclass; PROD/TEST named configs replace flat if/else constants | ## Context @@ -32,7 +33,7 @@ Before running on HPC, edit the path constants at the top of `run_small_molecule - **`small_molecule_binding.py`** — defines `SmallMoleculeBindingPipeline(ImpressBasePipeline)`, all step constants, ensemble utility functions (`_ca_rmsd`, `_seq_identity`, `_ensemble_selective_avg`), and the inner `_run_refine_cycle()` loop. All pipeline tasks (HPC and local analysis) are registered via `@self.auto_register_task()` inside `_register_real_tasks()`. The `run()` method drives a state-machine loop; `_run_refine_cycle()` handles the MPNN+PackMin inner loop with per-cycle sequence retry support. -- **`run_small_molecule_binding.py`** — entry point. Sets threshold constants, defines the `adaptive_decision()` callback, creates an `ImpressManager`, and launches via `manager.start(pipeline_setups=[...])`. +- **`run_small_molecule_binding.py`** — entry point. Defines the `RunConfig` dataclass and two named instances (`PROD`, `TEST`); selects between them via `IMPRESS_TEST_MODE`; defines the `adaptive_decision()` callback; creates an `ImpressManager` and launches via `manager.start(pipeline_setups=[...])`. ### Step constants (state-machine constants in `small_molecule_binding.py`) @@ -124,7 +125,7 @@ Ensemble similarity utilities (all in `small_molecule_binding.py`): | `fold_min_plddt` | 70.0 | minimum mean pLDDT | | `max_tasks` | 300 | maximum ensemble entries before stopping | -Threshold constants in `run_small_molecule_binding.py` override these defaults at `PipelineSetup` construction. +The `PROD` config in `run_small_molecule_binding.py` overrides these class defaults at `PipelineSetup` construction (e.g. `backbone_max_ca_deviation=1.0`, `fastrelax_max_interact=-8.0`, `fold_min_plddt=75.0`). The `TEST` config sets inert thresholds (everything passes) and `max_tasks=10` for integration testing. ### Output directory structure @@ -174,7 +175,7 @@ Steps communicate via `self.state`: ### Execution backends -`run_small_molecule_binding.py` has `LocalExecutionBackend(ProcessPoolExecutor())` active by default. `DragonExecutionBackendV3()` is commented out — swap it in for HPC production runs. +`run_small_molecule_binding.py` uses `DragonExecutionBackend` for HPC production runs. `LocalExecutionBackend(ProcessPoolExecutor())` can be swapped in for local testing. ### Pipeline inputs diff --git a/examples/small_molecule_binding/delta_env_setup.sh b/examples/small_molecule_binding/delta_env_setup.sh new file mode 100755 index 0000000..528b888 --- /dev/null +++ b/examples/small_molecule_binding/delta_env_setup.sh @@ -0,0 +1,252 @@ +#!/bin/bash +# ============================================================================= +# IMPRESS Small Molecule Binding environment setup — Delta HPC (NCSA) +# +# Creates a Python 3.11+ venv and installs all dependencies. +# +# Usage: +# export SCRATCH=/scratch/ +# bash delta_env_setup.sh [--env-dir DIR] [--impress-dir DIR] [--python PATH] +# +# Defaults: +# ENV_DIR = /u/$USER/ve/impress +# IMPRESS_DIR = $SCRATCH/$USER/IMPRESS +# python = auto-detected (python/3.11, cray-python/3.11.7, anaconda3) +# +# Tool directories (cloned by this script if absent): +# MPNN_DIR = $SCRATCH/$USER/LigandMPNN +# COLABFOLD_PATH = $SCRATCH/$USER/localcolabfold (used only for cache ref) +# COLABFOLD_CACHE_DIR= $SCRATCH/$USER/.cache/colabfold +# +# Foundry container (RFD3 backbone diffusion) is managed separately: +# Run pull_foundry.sh to build the sandbox tarball; delta_gpu_run.sh unpacks +# it to /tmp at job start. +# ============================================================================= +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + set -euo pipefail +fi + +# ── Require SCRATCH ─────────────────────────────────────────────────────────── +if [[ -z "${SCRATCH:-}" ]]; then + echo "ERROR: set the SCRATCH env var to your allocation scratch root, e.g.:" + echo " export SCRATCH=/scratch/" + echo " bash delta_env_setup.sh" + exit 1 +fi + +# ── Defaults / arg parsing ──────────────────────────────────────────────────── +ENV_DIR="${ENV_DIR:-/u/${USER}/ve/impress}" +IMPRESS_DIR="${IMPRESS_DIR:-${SCRATCH}/${USER}/IMPRESS}" +BASE_PY_OVERRIDE="" + +while [[ $# -gt 0 ]]; do + case $1 in + --env-dir) ENV_DIR="$2"; shift 2 ;; + --impress-dir) IMPRESS_DIR="$2"; shift 2 ;; + --python) BASE_PY_OVERRIDE="$2"; shift 2 ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done + +PY="${ENV_DIR}/bin/python" +PIP="${ENV_DIR}/bin/pip" + +MPNN_DIR="${MPNN_DIR:-${SCRATCH}/${USER}/LigandMPNN}" +COLABFOLD_CACHE_DIR="${COLABFOLD_CACHE_DIR:-${SCRATCH}/${USER}/.cache/colabfold}" + +echo "=================================================================" +echo " ENV_DIR = ${ENV_DIR}" +echo " IMPRESS_DIR = ${IMPRESS_DIR}" +echo " MPNN_DIR = ${MPNN_DIR}" +echo " COLABFOLD_CACHE = ${COLABFOLD_CACHE_DIR}" +echo "=================================================================" + +# ── 1. Create venv ──────────────────────────────────────────────────────────── +echo "" +echo "── Step 1: Creating venv ──" + +_find_python() { + for candidate in python3.12 python3.11 python3 python; do + local p + p=$(command -v "${candidate}" 2>/dev/null) || continue + local ver + ver=$("${p}" -c "import sys; v=sys.version_info; print(v.major*10+v.minor)" 2>/dev/null) || continue + [ "${ver}" -ge 311 ] && echo "${p}" && return 0 + done + return 1 +} + +if [ -n "${BASE_PY_OVERRIDE}" ]; then + BASE_PY="${BASE_PY_OVERRIDE}" + echo "Using Python override: ${BASE_PY}" +else + BASE_PY=$(_find_python || true) + if [ -z "${BASE_PY}" ]; then + echo "python3.11+ not in PATH — trying modules..." + for mod in python/3.12 python/3.11 cray-python/3.11.7 anaconda3; do + module load "${mod}" 2>/dev/null || true + BASE_PY=$(_find_python || true) + [ -n "${BASE_PY}" ] && echo " loaded module: ${mod}" && break + done + fi + if [ -z "${BASE_PY}" ]; then + echo "ERROR: no Python 3.11+ interpreter found." + echo " Pass an explicit interpreter: --python /path/to/python3.11" + echo " Or load a module manually before running this script." + exit 1 + fi +fi +echo "Using Python: ${BASE_PY} ($(${BASE_PY} --version))" + +if [ ! -x "${PY}" ]; then + "${BASE_PY}" -m venv "${ENV_DIR}" +else + echo "venv already exists at ${ENV_DIR}" +fi + +echo "Python: $("${PY}" --version)" + +# ── 2. Bootstrap pip ────────────────────────────────────────────────────────── +echo "" +echo "── Step 2: Bootstrapping pip ──" +"${PY}" -m pip install -q --upgrade pip wheel +"${PIP}" install -q --force-reinstall "setuptools<71" + +# ── 3. radical.asyncflow (PyPI) ────────────────────────────────────────────── +echo "" +echo "── Step 3: radical-asyncflow (PyPI) ──" +"${PIP}" install -q radical-asyncflow + +# ── 4. rhapsody-py (PyPI) ──────────────────────────────────────────────────── +echo "" +echo "── Step 4: rhapsody-py[dragon] (PyPI) ──" +"${PIP}" install -q "rhapsody-py[dragon,telemetry]" + +# ── 5. IMPRESS (local editable) ─────────────────────────────────────────────── +echo "" +echo "── Step 5: IMPRESS (editable) ──" +"${PIP}" install -q -e "${IMPRESS_DIR}" + +# ── 6. PyTorch (CUDA 12.1) — required by LigandMPNN ───────────────────────── +echo "" +echo "── Step 6: PyTorch (cu121) ──" +"${PIP}" install -q torch --index-url https://download.pytorch.org/whl/cu121 + +# ── 7. ColabFold + AlphaFold2 with pinned JAX versions ─────────────────────── +# +# Version constraints validated on Delta gpuA40x4 (CUDA 12.8 / cuDNN 9.25): +# +# colabfold 1.6.2 requires alphafold-colabfold==2.3.18 +# jax>=0.5.2,<0.11 +# +# alphafold-colabfold 2.3.18 is compatible with jaxlib 0.5.x but NOT with +# jaxlib 0.10.x (MSA feature shape mismatch at runtime). +# +# jax 0.5.2 / jaxlib 0.5.1 require nvidia-cudnn-cu12 >=9.1,<10.0. +# Upgrade cudnn to >=9.8.0 so jaxlib's cuDNN version check passes (jaxlib +# 0.5.1 links against cuDNN 9.x; runtime version must satisfy >=compiled). +# +echo "" +echo "── Step 7: ColabFold + AlphaFold2 (pinned JAX) ──" +# Install colabfold with the alphafold extra (pulls alphafold-colabfold 2.3.18, +# dm-haiku, dm-tree, ml-collections, absl-py). +"${PIP}" install -q "colabfold[alphafold]" +# Pin JAX to the era tested with alphafold-colabfold 2.3.18. +# jaxlib 0.5.2 does not exist on PyPI; 0.5.1 pairs with jax 0.5.2. +"${PIP}" install -q "jax[cuda12]==0.5.2" "jaxlib==0.5.1" +# Upgrade cuDNN so jaxlib's runtime check (>=compiled version) passes. +"${PIP}" install -q "nvidia-cudnn-cu12>=9.8.0,<10.0" + +# ── 8. LigandMPNN ───────────────────────────────────────────────────────────── +# +# LigandMPNN is run directly from its source tree (no package install). +# This step clones the repo; all Python dependencies (torch, ProDy, biopython, +# numpy) are already satisfied by the venv above. +# LigandMPNN's own requirements.txt pins older torch/cudnn versions — do NOT +# install it into this venv; the newer versions here are compatible at runtime. +# +echo "" +echo "── Step 8: LigandMPNN (clone) ──" +if [ ! -d "${MPNN_DIR}" ]; then + echo " Cloning LigandMPNN to ${MPNN_DIR}" + git clone https://github.com/dauparas/LigandMPNN "${MPNN_DIR}" +else + echo " LigandMPNN already at ${MPNN_DIR}, pulling latest" + git -C "${MPNN_DIR}" pull --ff-only || echo " (pull skipped — non-fast-forward or detached HEAD)" +fi +# Install ProDy and biopython (needed by LigandMPNN; torch already installed). +"${PIP}" install -q ProDy biopython + +# ── 9. gemmi — CIF.GZ parsing for backbone conversion ──────────────────────── +echo "" +echo "── Step 9: gemmi ──" +"${PIP}" install -q gemmi + +# ── 10. Additional dependencies ─────────────────────────────────────────────── +echo "" +echo "── Step 10: pandas + biopandas ──" +"${PIP}" install -q pandas biopandas + +# ── 11. PyRosetta ───────────────────────────────────────────────────────────── +echo "" +echo "── Step 11: PyRosetta ──" +"${PIP}" install -q pyrosetta-installer +"${PY}" -c "import pyrosetta_installer; pyrosetta_installer.install_pyrosetta()" + +# ── 12. ColabFold model weights ─────────────────────────────────────────────── +# +# Pre-download AlphaFold2 model weights to COLABFOLD_CACHE_DIR so compute +# nodes (no internet) find them at runtime. Run this step on a login node. +# +echo "" +echo "── Step 12: ColabFold model weights ──" +mkdir -p "${COLABFOLD_CACHE_DIR}" +echo " Downloading AlphaFold2 weights to ${COLABFOLD_CACHE_DIR} ..." +"${PY}" -c " +from colabfold.download import download_alphafold_params +download_alphafold_params('alphafold2', '${COLABFOLD_CACHE_DIR}') +print(' Weights downloaded.') +" + +# ── 13. Verify ──────────────────────────────────────────────────────────────── +echo "" +echo "── Step 13: Verifying installation ──" +_check() { + local label="$1"; shift + if out=$("$@" 2>&1); then + echo " ${label}: OK (${out})" + else + echo " WARNING: ${label} failed" + echo " ${out}" | head -3 + fi +} + +_check "radical.asyncflow" "${PY}" -c "import radical.asyncflow; print(radical.asyncflow.__version__)" +_check "rhapsody-py" "${PY}" -c "import rhapsody; print('ok')" +_check "impress" "${PY}" -c "import impress; print('ok')" +_check "torch" "${PY}" -c "import torch; print(torch.__version__)" +_check "jax" "${PY}" -c "import jax; print(jax.__version__)" +_check "colabfold" "${PY}" -c "import colabfold; print(colabfold.__version__)" +_check "alphafold" "${PY}" -c "import alphafold; print('ok')" +_check "gemmi" "${PY}" -c "import gemmi; print(gemmi.__version__)" +_check "pyrosetta" "${PY}" -c "import pyrosetta; print('ok')" +_check "ProDy" "${PY}" -c "import prody; print(prody.__version__)" +_check "LigandMPNN" test -d "${MPNN_DIR}" && echo "present" +_check "colabfold weights" test -d "${COLABFOLD_CACHE_DIR}/params" && echo "present" + +echo "" +echo "=================================================================" +echo "Setup complete." +echo "" +echo "Activate with:" +echo " source ${ENV_DIR}/bin/activate" +echo "" +echo "Run the pipeline:" +echo " export SCRATCH=${SCRATCH}" +echo " export SBATCH_ACCOUNT=bblj-delta-gpu" +echo " cd ${IMPRESS_DIR}/examples/small_molecule_binding" +echo " sbatch delta_gpu_run.sh" +echo "" +echo "Note: Foundry container (RFD3) is managed separately." +echo " Build once with: sbatch pull_foundry.sh" +echo "=================================================================" diff --git a/examples/small_molecule_binding/delta_gpu_run.sh b/examples/small_molecule_binding/delta_gpu_run.sh new file mode 100644 index 0000000..9b4852a --- /dev/null +++ b/examples/small_molecule_binding/delta_gpu_run.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# +# Small Molecule Binding Pipeline — SLURM batch script (Delta HPC / GPU) +# +# Set before calling sbatch (only SBATCH_ACCOUNT and SCRATCH are required; +# the rest default to standard Delta locations): +# export SBATCH_ACCOUNT=-delta-gpu +# export SCRATCH=/scratch/ +# +# Optional overrides (all have defaults based on SCRATCH/$USER): +# export MPNN_DIR=/path/to/LigandMPNN +# export COLABFOLD_PATH=/path/to/localcolabfold +# export COLABFOLD_CACHE_DIR=/path/to/colabfold_cache +# +# Foundry container (RFD3): +# The foundry sandbox is stored as a .tar.gz on scratch (built by pull_foundry.sh). +# This script extracts it to /tmp at job start (no scratch quota cost) and removes +# it on exit. Override FOUNDRY_TAR to point to a different archive, or set +# FOUNDRY_SIF_PATH directly to skip extraction entirely (e.g. a pre-extracted dir). +# +# Example: +# sbatch delta_gpu_run.sh +# sbatch delta_gpu_run.sh run_nonadaptive.py # non-adaptive runner +# +# Account: set SBATCH_ACCOUNT=-delta-gpu before calling sbatch +#SBATCH --partition=gpuA40x4 +#SBATCH --nodes=1 +#SBATCH --tasks-per-node=1 +#SBATCH --cpus-per-task=16 +#SBATCH --gpus-per-node=4 +#SBATCH --mem=220G +#SBATCH --time=02:30:00 +#SBATCH --job-name=impress_sm_binding +#SBATCH --mail-user=mg2347@soe.rutgers.edu +#SBATCH --mail-type=ALL +#SBATCH --output=logs/impress_%j.out +#SBATCH --error=logs/impress_%j.err +# NOTE: logs/ must exist before sbatch is called. Create it once with: +# mkdir -p /logs + +set -e + +# ── Sanity checks ───────────────────────────────────────────────────────────── +if [ -z "${SBATCH_ACCOUNT:-}${SLURM_JOB_ACCOUNT:-}" ]; then + echo "WARNING: SBATCH_ACCOUNT is not set — job may be charged to default account." +fi +echo "Account: ${SLURM_JOB_ACCOUNT:-unknown}" + +if [ -z "${SCRATCH:-}" ]; then + echo "ERROR: SCRATCH is not set." + echo " export SCRATCH=/scratch/ && sbatch delta_gpu_run.sh" + exit 1 +fi + +# ── System library paths (Delta-specific, required by Dragon) ───────────────── +export CUDA_HOME=/opt/nvidia/hpc_sdk/Linux_x86_64/25.3/cuda/12.8 +export MPI_LIB=/opt/cray/pe/mpich/8.1.32/ofi/gnu/11.2/lib-abi-mpich +export FAB_LIB=/opt/cray/libfabric/1.22.0/lib64 +export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH:-} + +# ── Environment ─────────────────────────────────────────────────────────────── +IMPRESS_VENV="${IMPRESS_VENV:-${HOME}/ve/impress}" +unset SLURM_EXPORT_ENV +source "${IMPRESS_VENV}/bin/activate" +dragon-config add --ofi-runtime-lib="${FAB_LIB}" + +# ── Tool paths (read by SmallMoleculeBindingPipeline via env vars) ───────────── +# These are picked up by the pipeline's __init__ when not passed as kwargs. +export MPNN_DIR="${MPNN_DIR:-${SCRATCH}/${USER}/LigandMPNN}" + +export COLABFOLD_PATH="${COLABFOLD_PATH:-${SCRATCH}/${USER}/localcolabfold}" + +# ColabFold model weights cache — kept on scratch to avoid home quota exhaustion. +# Pre-download once on login node: +# export COLABFOLD_CACHE_DIR=${SCRATCH}/${USER}/.cache/colabfold +# python -c "from colabfold.download import download_alphafold_params; \ +# download_alphafold_params('alphafold2', '${COLABFOLD_CACHE_DIR}')" +export COLABFOLD_CACHE_DIR="${COLABFOLD_CACHE_DIR:-${SCRATCH}/${USER}/.cache/colabfold}" +mkdir -p "${COLABFOLD_CACHE_DIR}" + +# ── Foundry sandbox: extract to /tmp at job start, clean up on exit ─────────── +# Extracting to /tmp avoids the scratch quota. Compute nodes have ample /tmp +# space that is not quota-counted. If FOUNDRY_SIF_PATH is already set (e.g. +# a pre-built .sif or a persistent sandbox on a large allocation), extraction +# is skipped entirely. +if [ -z "${FOUNDRY_SIF_PATH:-}" ]; then + FOUNDRY_TAR="${FOUNDRY_TAR:-${SCRATCH}/${USER}/foundry_sandbox.tar.gz}" + if [ ! -f "${FOUNDRY_TAR}" ]; then + echo "ERROR: foundry sandbox tarball not found: ${FOUNDRY_TAR}" + echo " Build it first: sbatch pull_foundry.sh" + exit 1 + fi + _FOUNDRY_TMP="/tmp/foundry_${SLURM_JOB_ID:-$$}" + echo "Extracting foundry sandbox from ${FOUNDRY_TAR} to ${_FOUNDRY_TMP} ..." + mkdir -p "${_FOUNDRY_TMP}" + tar -xzf "${FOUNDRY_TAR}" -C "${_FOUNDRY_TMP}" --strip-components=1 + export FOUNDRY_SIF_PATH="${_FOUNDRY_TMP}" + # shellcheck disable=SC2064 + trap "echo 'Removing ${_FOUNDRY_TMP}'; rm -rf '${_FOUNDRY_TMP}'" EXIT +fi + +echo "MPNN_DIR: ${MPNN_DIR}" +echo "FOUNDRY_SIF_PATH: ${FOUNDRY_SIF_PATH}" +echo "COLABFOLD_PATH: ${COLABFOLD_PATH}" +echo "COLABFOLD_CACHE: ${COLABFOLD_CACHE_DIR}" + +# ── Tool existence checks ────────────────────────────────────────────────────── +if [ ! -d "${MPNN_DIR}" ]; then + echo "ERROR: MPNN_DIR does not exist: ${MPNN_DIR}" + echo " Clone LigandMPNN: git clone https://github.com/dauparas/LigandMPNN ${MPNN_DIR}" + exit 1 +fi + +# ── Working directory ───────────────────────────────────────────────────────── +WORKDIR="${IMPRESS_SCRIPTS_DIR:-${SCRATCH}/${USER}/IMPRESS/examples/small_molecule_binding}" +cd "${WORKDIR}" +mkdir -p logs + +# IMPRESS_WORK_DIR: where pipeline task dirs (p1/, p2/, …) are written. +# Defaults to logs/ so all run artifacts stay out of the source tree and are +# covered by .gitignore. Override to write outputs elsewhere. +export IMPRESS_WORK_DIR="${IMPRESS_WORK_DIR:-${WORKDIR}/logs}" +mkdir -p "${IMPRESS_WORK_DIR}" + +# IMPRESS_TEST_MODE=1: 2 pipelines, inert thresholds, max_tasks=10. +# Runs one full rfd3→mpnn→fastrelax→filter_shape→af2 cycle to verify the +# end-to-end path without looping. Set before sbatch: +# IMPRESS_TEST_MODE=1 sbatch delta_gpu_run.sh +export IMPRESS_TEST_MODE="${IMPRESS_TEST_MODE:-0}" +echo "TEST_MODE: ${IMPRESS_TEST_MODE}" + +# ── Run ─────────────────────────────────────────────────────────────────────── +# asyncflow session dirs now go to /tmp (node-local, no quota) via IMPRESS_SESSION_DIR. + +# -s = single-node Dragon runtime; -m = multi-node (uses MPI/OFI fabric). +if [ "${SLURM_NNODES:-1}" -gt 1 ]; then + DRAGON_MODE="-m" +else + DRAGON_MODE="-s" +fi + +rm -f ddict_orc* + +RUNNER="${1:-run_small_molecule_binding.py}" +echo "Running: dragon ${DRAGON_MODE} ${RUNNER} (nodes=${SLURM_NNODES:-1})" +dragon ${DRAGON_MODE} "${RUNNER}" + +echo "=== Small Molecule Binding pipeline done: $(date) ===" diff --git a/examples/small_molecule_binding/pull_foundry.sh b/examples/small_molecule_binding/pull_foundry.sh new file mode 100644 index 0000000..8183bee --- /dev/null +++ b/examples/small_molecule_binding/pull_foundry.sh @@ -0,0 +1,35 @@ +#!/bin/bash +#SBATCH --account=bblj-delta-gpu +#SBATCH --partition=gpuA40x4 +#SBATCH --nodes=1 +#SBATCH --ntasks=1 +#SBATCH --cpus-per-task=8 +#SBATCH --mem=128G +#SBATCH --gpus-per-node=1 +#SBATCH --time=00:30:00 +#SBATCH --job-name=pull_foundry +#SBATCH --output=slurm-%j.out +#SBATCH --error=slurm-%j.err + +set -euo pipefail +ulimit -c 0 # disable core dumps + +export APPTAINER_CACHEDIR=/scratch/bblj/mgoliyad1/.apptainer_cache +export APPTAINER_TMPDIR=/tmp/apptainer_$$ +mkdir -p "$APPTAINER_TMPDIR" + +# Build as sandbox (directory) to /tmp — no mksquashfs involved. +# Then tar to a single archive on scratch for storage. +SANDBOX=/tmp/foundry_sandbox_$$ +DEST_TAR=/scratch/bblj/mgoliyad1/foundry_sandbox.tar.gz + +echo "=== Building sandbox to /tmp ===" +apptainer build --sandbox "$SANDBOX" docker://rosettacommons/foundry + +echo "=== Compressing sandbox to scratch ===" +tar -czf "$DEST_TAR" -C /tmp "foundry_sandbox_$$" + +echo "Done: $(ls -lh "$DEST_TAR")" +rm -rf "$SANDBOX" + +rm -rf "$APPTAINER_TMPDIR" diff --git a/examples/small_molecule_binding/run_nonadaptive.py b/examples/small_molecule_binding/run_nonadaptive.py index bc2dd76..2f87e6e 100644 --- a/examples/small_molecule_binding/run_nonadaptive.py +++ b/examples/small_molecule_binding/run_nonadaptive.py @@ -1,4 +1,5 @@ import asyncio +import os from typing import List from rhapsody.backends import DragonExecutionBackend @@ -49,8 +50,20 @@ async def nonadaptive_decision(pipeline: SmallMoleculeBindingPipeline) -> None: ) +# Indices of pipelines to run — corresponds to the protein IDs selected for +# this campaign (non-contiguous because some were dropped after earlier runs). +PIPELINE_INDICES = [1, 2, 4, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 23, 26, 27, 30, 32] + + async def impress_smallmol_nonadaptive() -> None: """Execute the small-molecule binding pipeline without adaptive routing.""" + examples_dir = os.path.dirname(os.path.abspath(__file__)) + work_dir = os.environ.get( + "IMPRESS_WORK_DIR", os.path.join(examples_dir, "logs") + ) + os.makedirs(work_dir, exist_ok=True) + input_dir = os.path.join(examples_dir, "p1_in") + backend = await DragonExecutionBackend() manager: ImpressManager = ImpressManager(execution_backend=backend) @@ -60,6 +73,9 @@ async def impress_smallmol_nonadaptive() -> None: type=SmallMoleculeBindingPipeline, adaptive_fn=nonadaptive_decision, kwargs={ + "base_path": work_dir, + "scripts_path": os.path.join(examples_dir, "scripts"), + "input_dir": input_dir, "backbone_max_ca_deviation": BACKBONE_MAX_CA_DEVIATION, "backbone_min_ss_fraction": BACKBONE_MIN_SS_FRACTION, "fastrelax_max_fa_rep": FASTRELAX_MAX_FA_REP, @@ -71,7 +87,7 @@ async def impress_smallmol_nonadaptive() -> None: "num_refine_cycles": 2, } ) - for i in [1,2,4,6,7,8,10,11,12,13,14,15,16,18,19,20,23,26,27,30,32] + for i in PIPELINE_INDICES ] await manager.start(pipeline_setups=pipeline_setups) diff --git a/examples/small_molecule_binding/run_small_molecule_binding.py b/examples/small_molecule_binding/run_small_molecule_binding.py index 41df18d..a0983cc 100644 --- a/examples/small_molecule_binding/run_small_molecule_binding.py +++ b/examples/small_molecule_binding/run_small_molecule_binding.py @@ -1,10 +1,11 @@ import asyncio import os +from dataclasses import dataclass from typing import List from rhapsody.backends import DragonExecutionBackend -from impress import ImpressManager, PipelineSetup +from impress import GPUPolicy, _find_gpus, _make_policy, ImpressManager, PipelineSetup from small_molecule_binding import ( SmallMoleculeBindingPipeline, STEP_DONE, STEP_RFD3, STEP_MPNN, STEP_FASTRELAX, STEP_INTERFACE, STEP_AF2, @@ -17,14 +18,57 @@ import rhapsody rhapsody.enable_logging(level=logging.INFO) -# ── Per-step quality thresholds ──────────────────────────────────────────── -BACKBONE_MAX_CA_DEVIATION = 1.0 -BACKBONE_MIN_SS_FRACTION = 0.5 -FASTRELAX_MAX_FA_REP = 100.0 # fa_rep REU -FASTRELAX_MAX_SCORE = -250.0 # total_score REU (was 0.0 — inert; data range -193 to -510) -FASTRELAX_MAX_INTERACT = -8.0 # interaction energy REU (was absent/0.0 — inert; p75=-8.8) -INTERFACE_MIN_SC = 0.55 # shape complementarity (was 0.35 — passed 100%; data min=0.37) -FOLD_MIN_PLDDT = 75.0 # mean pLDDT (was 70.0; ~70% of AF2 runs exceed 75) + +@dataclass +class RunConfig: + n_pipelines: int + max_tasks: int + # backbone + backbone_max_ca_deviation: float + backbone_min_ss_fraction: float + # fastrelax + fastrelax_max_fa_rep: float + fastrelax_max_score: float # total_score REU + fastrelax_max_interact: float # interaction energy REU + # interface + interface_min_sc: float # shape complementarity + # fold + fold_min_plddt: float # mean pLDDT; init is -1.0 so -1.0 = always pass + # diffusion / refinement + diffusion_batch_size: int + num_refine_cycles: int + + +PROD = RunConfig( + n_pipelines = 4, + max_tasks = 300, + backbone_max_ca_deviation = 1.0, + backbone_min_ss_fraction = 0.5, + fastrelax_max_fa_rep = 100.0, + fastrelax_max_score = -250.0, # data range -193 to -510 + fastrelax_max_interact = -8.0, # p75 = -8.8 + interface_min_sc = 0.55, + fold_min_plddt = 75.0, + diffusion_batch_size = 4, + num_refine_cycles = 2, +) + +# Inert thresholds — everything passes; low task budget for one full cycle. +TEST = RunConfig( + n_pipelines = 2, + max_tasks = 10, + backbone_max_ca_deviation = 9999.0, + backbone_min_ss_fraction = 0.0, + fastrelax_max_fa_rep = 9999.0, + fastrelax_max_score = 9999.0, + fastrelax_max_interact = 9999.0, + interface_min_sc = 0.0, + fold_min_plddt = -1.0, + diffusion_batch_size = 1, + num_refine_cycles = 1, +) + +cfg = TEST if os.getenv("IMPRESS_TEST_MODE", "0") == "1" else PROD async def adaptive_decision(pipeline: SmallMoleculeBindingPipeline) -> None: @@ -137,32 +181,48 @@ def _prior(ttype): async def impress_smallmol_bind() -> None: """Execute the small-molecule binding pipeline.""" + # Resolve paths before launching Dragon (os.getcwd() is the examples dir). + examples_dir = os.path.dirname(os.path.abspath(__file__)) + work_dir = os.environ.get( + "IMPRESS_WORK_DIR", os.path.join(examples_dir, "logs") + ) + os.makedirs(work_dir, exist_ok=True) + # Input data lives in the source tree; pass as absolute so it resolves + # correctly regardless of what base_path / work_dir is set to. + input_dir = os.path.join(examples_dir, "p1_in") + #backend = await LocalExecutionBackend(ProcessPoolExecutor()) backend = await DragonExecutionBackend() manager: ImpressManager = ImpressManager(execution_backend=backend) + all_gpus = _find_gpus() + pipeline_setups: List[PipelineSetup] = [ PipelineSetup( name=f"p{str(i)}", type=SmallMoleculeBindingPipeline, adaptive_fn=adaptive_decision, kwargs={ - "backbone_max_ca_deviation": BACKBONE_MAX_CA_DEVIATION, - "backbone_min_ss_fraction": BACKBONE_MIN_SS_FRACTION, - "fastrelax_max_fa_rep": FASTRELAX_MAX_FA_REP, - "fastrelax_max_total_score": FASTRELAX_MAX_SCORE, - "fastrelax_max_interact": FASTRELAX_MAX_INTERACT, - "interface_min_sc": INTERFACE_MIN_SC, - "fold_min_plddt": FOLD_MIN_PLDDT, - "diffusion_batch_size": 4, - "num_refine_cycles": 2, + "base_path": work_dir, + "scripts_path": os.path.join(examples_dir, "scripts"), + "input_dir": input_dir, + "backbone_max_ca_deviation": cfg.backbone_max_ca_deviation, + "backbone_min_ss_fraction": cfg.backbone_min_ss_fraction, + "fastrelax_max_fa_rep": cfg.fastrelax_max_fa_rep, + "fastrelax_max_total_score": cfg.fastrelax_max_score, + "fastrelax_max_interact": cfg.fastrelax_max_interact, + "interface_min_sc": cfg.interface_min_sc, + "fold_min_plddt": cfg.fold_min_plddt, + "diffusion_batch_size": cfg.diffusion_batch_size, + "num_refine_cycles": cfg.num_refine_cycles, + "max_tasks": cfg.max_tasks, + "policy": _make_policy(all_gpus, i - 1), } ) - for i in range(1,9) + for i in range(1, cfg.n_pipelines + 1) ] await manager.start(pipeline_setups=pipeline_setups) - await manager.flow.shutdown() if __name__ == "__main__": diff --git a/examples/small_molecule_binding/scripts/af2.sh b/examples/small_molecule_binding/scripts/af2.sh index 2f051d1..fcdbf9b 100755 --- a/examples/small_molecule_binding/scripts/af2.sh +++ b/examples/small_molecule_binding/scripts/af2.sh @@ -35,7 +35,8 @@ data_dir="${COLABFOLD_CACHE_DIR:-${HOME}/.cache/colabfold}" "$colabfold_bin" \ --model-type alphafold2 \ - --num-models 1 \ + --msa-mode single_sequence \ + --num-models "${AF2_NUM_MODELS:-1}" \ --data "$data_dir" \ --rank auto \ --random-seed 999 \ diff --git a/examples/small_molecule_binding/scripts/mpnn_wrapper.sh b/examples/small_molecule_binding/scripts/mpnn_wrapper.sh deleted file mode 100755 index 545901b..0000000 --- a/examples/small_molecule_binding/scripts/mpnn_wrapper.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -# run ligandmpnn - -pdb_path=$1 -output_dir=$2 -lmpnn_dir=$3 - -echo "A16" > fixed_residues.txt -FIXED=`cat fixed_residues.txt` -set -x -python $lmpnn_dir/run.py \ - --model_type "ligand_mpnn" \ - --checkpoint_path_sc $lmpnn_dir/model_params/ligandmpnn_sc_v_32_002_16.pt \ - --checkpoint_ligand_mpnn $lmpnn_dir/model_params/ligandmpnn_v_32_010_25.pt \ - --seed 111 \ - --pdb_path $pdb_path \ - --out_folder $output_dir \ - --pack_side_chains 1 \ - --number_of_batches 1 \ - --batch_size 1 \ - --number_of_packs_per_design 1 \ - --pack_with_ligand_context 1 \ - --fixed_residues "$FIXED" \ - --repack_everything 1 \ - --temperature 0.1 -# --bias_AA "A:10.0" - - diff --git a/examples/small_molecule_binding/scripts/packmin.py b/examples/small_molecule_binding/scripts/packmin.py index f417ec8..d32bbdd 100644 --- a/examples/small_molecule_binding/scripts/packmin.py +++ b/examples/small_molecule_binding/scripts/packmin.py @@ -12,16 +12,7 @@ #Protocol Includes from pyrosetta.rosetta.protocols import minimization_packing as pack_min -''' -When downloading a new PDB file, do a pack_min minimization with coordinate constraints and a ligand. -Requires a PDB file input. - -Options: -Name (-n, string): change the output PDB name from [original_name]_relaxed.pdb -Score function (-sf, string): change the score function from the default of ref2015_cst -Catalytic residues (-cat, int, multiple accepted): list residues that should not be moved -''' def parse_args(): parser = argparse.ArgumentParser() diff --git a/examples/small_molecule_binding/small_molecule_binding.py b/examples/small_molecule_binding/small_molecule_binding.py index 3367c23..d514945 100644 --- a/examples/small_molecule_binding/small_molecule_binding.py +++ b/examples/small_molecule_binding/small_molecule_binding.py @@ -145,7 +145,15 @@ def __init__(self, name, flow, configs=None, **kwargs): self.scripts_path = kwargs.get( "scripts_path", os.path.join(self.base_path, "scripts") ) - self.pipeline_inputs = os.path.join(self.base_path, f"{self.name}_in") + # input_dir: explicit input directory name (relative to base_path) or + # absolute path. Falls back to "_in" when not provided. + _input_dir = kwargs.get("input_dir", "") + if _input_dir and os.path.isabs(_input_dir): + self.pipeline_inputs = _input_dir + elif _input_dir: + self.pipeline_inputs = os.path.join(self.base_path, _input_dir) + else: + self.pipeline_inputs = os.path.join(self.base_path, f"{self.name}_in") self.mpnn_dir = kwargs.get("mpnn_dir") or os.environ.get("MPNN_DIR") if not self.mpnn_dir: raise ValueError("mpnn_dir must be supplied via kwarg or MPNN_DIR env var") @@ -187,15 +195,6 @@ def __init__(self, name, flow, configs=None, **kwargs): self.next_step = STEP_RFD3 self._current_cycle_i = 0 # set by run() before each mpnn call - # ── GPU env helper ───────────────────────────────────────────────────── - - def _gpu_env(self): - """Return subprocess env with CUDA_VISIBLE_DEVICES set from Dragon Policy gpu_affinity.""" - env = {**os.environ} - if self.policy and self.policy.gpu_affinity: - env["CUDA_VISIBLE_DEVICES"] = ",".join(str(g) for g in self.policy.gpu_affinity) - return env - # ── Task registration ────────────────────────────────────────────────── def register_pipeline_tasks(self): @@ -614,6 +613,12 @@ async def analysis_fold(): if 'scores' in f and f.endswith('.json') ] + if not score_files: + raise RuntimeError( + f"af2 produced no score files in {out_dir} — " + "GPU/cuDNN failure (Foundry container may still hold GPU memory)" + ) + best_plddt = -1.0 best_model = None for sf in score_files: diff --git a/src/impress/__init__.py b/src/impress/__init__.py index 89dd8d3..996526d 100644 --- a/src/impress/__init__.py +++ b/src/impress/__init__.py @@ -1,10 +1,14 @@ from __future__ import annotations +from impress.gpu import GPUPolicy, _find_gpus, _make_policy from impress.impress_manager import ImpressManager from impress.pipelines.impress_pipeline import ImpressBasePipeline from impress.pipelines.setup import PipelineSetup __all__ = [ + "GPUPolicy", + "_find_gpus", + "_make_policy", "ImpressManager", "ImpressBasePipeline", "PipelineSetup", diff --git a/src/impress/gpu.py b/src/impress/gpu.py new file mode 100644 index 0000000..a429de0 --- /dev/null +++ b/src/impress/gpu.py @@ -0,0 +1,25 @@ +import os +from dataclasses import dataclass, field + + +@dataclass +class GPUPolicy: + gpu_affinity: list = field(default_factory=list) + + +def _find_gpus() -> list: + cuda_visible = os.environ.get("CUDA_VISIBLE_DEVICES", "") + if cuda_visible: + return [int(g) for g in cuda_visible.split(",") if g.strip().isdigit()] + try: + from dragon.native.machine import System + sys_info = System() + return [gpu for node in sys_info.nodes for gpu in node.gpus] + except Exception: + pass + return [0, 1, 2, 3] # gpuA40x4 default + + +def _make_policy(all_gpus: list, idx: int, n_gpus: int = 1) -> GPUPolicy: + assigned = [all_gpus[(idx + j) % len(all_gpus)] for j in range(n_gpus)] + return GPUPolicy(gpu_affinity=assigned) diff --git a/src/impress/impress_manager.py b/src/impress/impress_manager.py index ec8f3de..9c100f3 100644 --- a/src/impress/impress_manager.py +++ b/src/impress/impress_manager.py @@ -1,4 +1,6 @@ import asyncio +import os +import tempfile from collections.abc import Awaitable from typing import Any, Callable, Optional, Union @@ -79,7 +81,9 @@ def submit_new_pipelines( of ImpressBasePipeline """ if self.flow is None: - raise RuntimeError("ImpressManager.start() must be called before submit_new_pipelines()") + raise RuntimeError( + "ImpressManager.start() must be called before submit_new_pipelines()" + ) for setup_input in pipeline_setups: # Normalize to PipelineSetup object setup = self._normalize_pipeline_setup(setup_input) @@ -139,13 +143,19 @@ async def start( """ self.logger.separator("IMPRESS MANAGER STARTING") + # Write asyncflow session dirs to /tmp (node-local, no quota) instead of + # cwd on scratch, which exhausts inodes over many runs. + _session_base = os.environ.get("IMPRESS_SESSION_DIR", tempfile.gettempdir()) self.flow = await WorkflowEngine.create( - backend=self.execution_backend + backend=self.execution_backend, + work_dir=_session_base, ) try: if self._telemetry_config: - self.telemetry = await self.flow.start_telemetry(**self._telemetry_config) + self.telemetry = await self.flow.start_telemetry( + **self._telemetry_config + ) for fn in self._telemetry_subscribers: self.telemetry.subscribe(fn) @@ -158,7 +168,7 @@ async def start( completed_pipelines: list[tuple] = [] for pipeline, pipeline_future in list(self.pipeline_tasks.items()): - # Check if pipeline needs adaptive step and isn't already running one + # Check if pipeline needs adaptive step and isn't running one yet if ( getattr(pipeline, "invoke_adaptive_step", False) and pipeline not in self.adaptive_tasks @@ -170,10 +180,14 @@ async def start( any_activity = True # Check if pipeline has new config ready - config: Optional[dict[str, Any]] = pipeline.get_child_pipeline_request() + config: Optional[dict[str, Any]] = ( + pipeline.get_child_pipeline_request() + ) if config: - self.logger.child_pipeline_submitted(config["name"], pipeline.name) + self.logger.child_pipeline_submitted( + config["name"], pipeline.name + ) # Convert dict to PipelineSetup for consistency child_setup = PipelineSetup.from_dict(config) self.new_pipeline_buffer.append(child_setup) @@ -189,7 +203,7 @@ async def start( # Check if pipeline is done - but only mark as completed # if adaptive task is also done if pipeline_future.done(): - # If there's an adaptive task running, don't mark as completed yet + # Adaptive task still running — wait before marking completed if pipeline in self.adaptive_tasks: adaptive_task = self.adaptive_tasks[pipeline] if not adaptive_task.done(): diff --git a/src/impress/pipelines/impress_pipeline.py b/src/impress/pipelines/impress_pipeline.py index d2e70d5..f828250 100644 --- a/src/impress/pipelines/impress_pipeline.py +++ b/src/impress/pipelines/impress_pipeline.py @@ -1,4 +1,5 @@ import asyncio +import os from abc import ABC, abstractmethod from typing import Any @@ -90,12 +91,19 @@ def register_pipeline_tasks(self): """Register pipeline tasks - must be implemented by subclasses""" pass + def _gpu_env(self) -> dict: + env = {**os.environ} + policy = getattr(self, "policy", None) + if policy and getattr(policy, "gpu_affinity", None): + env["CUDA_VISIBLE_DEVICES"] = ",".join(str(g) for g in policy.gpu_affinity) + return env + # Optional methods that subclasses can override async def get_scores_map(self): """Optional: Return scores mapping""" return {} - async def finalize(self): + async def finalize(self): # noqa: B027 """Optional: Cleanup or finalization logic""" pass diff --git a/src/impress/utils/logger.py b/src/impress/utils/logger.py index 117b0a7..4212385 100644 --- a/src/impress/utils/logger.py +++ b/src/impress/utils/logger.py @@ -34,7 +34,13 @@ class LogLevel(Enum): class ImpressLogger: - _LEVEL_ORDER = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR, LogLevel.CRITICAL] + _LEVEL_ORDER = [ + LogLevel.DEBUG, + LogLevel.INFO, + LogLevel.WARNING, + LogLevel.ERROR, + LogLevel.CRITICAL, + ] def __init__(self, name="ImpressManager", use_colors=True, output_stream=None, min_level: LogLevel = LogLevel.DEBUG): @@ -105,31 +111,41 @@ def _write_log(self, message): def debug(self, message, component="manager", pipeline_name=None): if not self._is_enabled(LogLevel.DEBUG): return - formatted = self._format_message(LogLevel.DEBUG, component, message, pipeline_name) + formatted = self._format_message( + LogLevel.DEBUG, component, message, pipeline_name + ) self._write_log(formatted) def info(self, message, component="manager", pipeline_name=None): if not self._is_enabled(LogLevel.INFO): return - formatted = self._format_message(LogLevel.INFO, component, message, pipeline_name) + formatted = self._format_message( + LogLevel.INFO, component, message, pipeline_name + ) self._write_log(formatted) def warning(self, message, component="manager", pipeline_name=None): if not self._is_enabled(LogLevel.WARNING): return - formatted = self._format_message(LogLevel.WARNING, component, message, pipeline_name) + formatted = self._format_message( + LogLevel.WARNING, component, message, pipeline_name + ) self._write_log(formatted) def error(self, message, component="manager", pipeline_name=None): if not self._is_enabled(LogLevel.ERROR): return - formatted = self._format_message(LogLevel.ERROR, component, message, pipeline_name) + formatted = self._format_message( + LogLevel.ERROR, component, message, pipeline_name + ) self._write_log(formatted) def critical(self, message, component="manager", pipeline_name=None): if not self._is_enabled(LogLevel.CRITICAL): return - formatted = self._format_message(LogLevel.CRITICAL, component, message, pipeline_name) + formatted = self._format_message( + LogLevel.CRITICAL, component, message, pipeline_name + ) self._write_log(formatted) def pipeline_started(self, pipeline_name): diff --git a/tests/conftest.py b/tests/conftest.py index c35a0f5..cb6ad28 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ # tests/conftest.py -import pytest import shutil from pathlib import Path - from unittest.mock import Mock + +import pytest + from impress import ImpressManager diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py new file mode 100644 index 0000000..930d5b1 --- /dev/null +++ b/tests/unit/test_logger.py @@ -0,0 +1,278 @@ +import io +import sys + +from impress.utils.logger import ImpressLogger, LogLevel + + +class TestImpressLoggerInit: + def test_default_init(self): + logger = ImpressLogger() + assert logger.name == "ImpressManager" + assert logger.use_colors is True + assert logger.output_stream is sys.stdout + assert logger.min_level == LogLevel.DEBUG + + def test_custom_stream(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream) + assert logger.output_stream is stream + + def test_custom_min_level(self): + logger = ImpressLogger(min_level=LogLevel.WARNING) + assert logger.min_level == LogLevel.WARNING + + def test_custom_name(self): + logger = ImpressLogger(name="my_pipeline") + assert logger.name == "my_pipeline" + + +class TestLogLevelFiltering: + def test_is_enabled_at_exact_level(self): + logger = ImpressLogger(min_level=LogLevel.INFO) + assert logger._is_enabled(LogLevel.INFO) is True + + def test_is_enabled_above_min(self): + logger = ImpressLogger(min_level=LogLevel.INFO) + assert logger._is_enabled(LogLevel.WARNING) is True + assert logger._is_enabled(LogLevel.ERROR) is True + assert logger._is_enabled(LogLevel.CRITICAL) is True + + def test_is_disabled_below_min(self): + logger = ImpressLogger(min_level=LogLevel.INFO) + assert logger._is_enabled(LogLevel.DEBUG) is False + + def test_debug_suppressed_at_info_level(self): + stream = io.StringIO() + logger = ImpressLogger( + output_stream=stream, min_level=LogLevel.INFO, use_colors=False + ) + logger.debug("should not appear") + assert stream.getvalue() == "" + + def test_info_written_at_info_level(self): + stream = io.StringIO() + logger = ImpressLogger( + output_stream=stream, min_level=LogLevel.INFO, use_colors=False + ) + logger.info("should appear") + assert "should appear" in stream.getvalue() + + def test_warning_suppressed_below_min(self): + stream = io.StringIO() + logger = ImpressLogger( + output_stream=stream, min_level=LogLevel.ERROR, use_colors=False + ) + logger.warning("should not appear") + assert stream.getvalue() == "" + + def test_all_levels_write_at_debug_min(self): + stream = io.StringIO() + logger = ImpressLogger( + output_stream=stream, min_level=LogLevel.DEBUG, use_colors=False + ) + logger.debug("d") + logger.info("i") + logger.warning("w") + logger.error("e") + logger.critical("c") + output = stream.getvalue() + assert "d" in output + assert "i" in output + assert "w" in output + assert "e" in output + assert "c" in output + + def test_critical_only_at_critical_min(self): + stream = io.StringIO() + logger = ImpressLogger( + output_stream=stream, min_level=LogLevel.CRITICAL, use_colors=False + ) + logger.debug("d") + logger.info("i") + logger.warning("w") + logger.error("e") + logger.critical("c") + output = stream.getvalue() + assert "d" not in output + assert "i" not in output + assert "w" not in output + assert "e" not in output + assert "c" in output + + +class TestOutputStreamRespected: + def test_error_writes_to_output_stream_not_stderr(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream, use_colors=False) + logger.error("an error") + assert "an error" in stream.getvalue() + + def test_critical_writes_to_output_stream(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream, use_colors=False) + logger.critical("critical msg") + assert "critical msg" in stream.getvalue() + + def test_custom_stream_receives_all_output(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream, use_colors=False) + logger.info("info line") + logger.error("error line") + logger.warning("warn line") + output = stream.getvalue() + assert "info line" in output + assert "error line" in output + assert "warn line" in output + + +class TestColors: + def test_no_ansi_when_colors_off(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream, use_colors=False) + logger.info("hello") + assert "\033[" not in stream.getvalue() + + def test_ansi_present_when_colors_on(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream, use_colors=True) + logger.info("hello") + assert "\033[" in stream.getvalue() + + def test_colorize_returns_plain_when_off(self): + logger = ImpressLogger(use_colors=False) + result = logger._colorize("text", "\033[31m") + assert result == "text" + + def test_colorize_wraps_when_on(self): + logger = ImpressLogger(use_colors=True) + result = logger._colorize("text", "\033[31m") + assert result.startswith("\033[31m") + assert "text" in result + + +class TestHighLevelMethods: + def _make_logger(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream, use_colors=False) + return logger, stream + + def test_pipeline_started(self): + logger, stream = self._make_logger() + logger.pipeline_started("my_pipe") + assert "my_pipe" in stream.getvalue() + + def test_pipeline_completed(self): + logger, stream = self._make_logger() + logger.pipeline_completed("my_pipe") + assert "my_pipe" in stream.getvalue() + + def test_pipeline_failed(self): + logger, stream = self._make_logger() + logger.pipeline_failed("bad_pipe", ValueError("oops")) + out = stream.getvalue() + assert "bad_pipe" in out + assert "oops" in out + + def test_pipeline_killed(self): + logger, stream = self._make_logger() + logger.pipeline_killed("dead_pipe") + assert "dead_pipe" in stream.getvalue() + + def test_adaptive_started(self): + logger, stream = self._make_logger() + logger.adaptive_started("p1") + assert "p1" in stream.getvalue() + + def test_adaptive_completed(self): + logger, stream = self._make_logger() + logger.adaptive_completed("p1") + assert "p1" in stream.getvalue() + + def test_adaptive_failed(self): + logger, stream = self._make_logger() + logger.adaptive_failed("p1", "bad fn") + out = stream.getvalue() + assert "p1" in out + assert "bad fn" in out + + def test_child_pipeline_submitted(self): + logger, stream = self._make_logger() + logger.child_pipeline_submitted("child", "parent") + out = stream.getvalue() + assert "child" in out + assert "parent" in out + + def test_manager_starting(self): + logger, stream = self._make_logger() + logger.manager_starting(5) + assert "5" in stream.getvalue() + + def test_manager_exiting(self): + logger, stream = self._make_logger() + logger.manager_exiting() + assert stream.getvalue() != "" + + def test_activity_summary_suppressed_at_info(self): + stream = io.StringIO() + logger = ImpressLogger( + output_stream=stream, use_colors=False, min_level=LogLevel.INFO + ) + logger.activity_summary(3, 1, 2) + assert stream.getvalue() == "" # activity_summary is DEBUG level + + def test_activity_summary_written_at_debug(self): + stream = io.StringIO() + logger = ImpressLogger( + output_stream=stream, use_colors=False, min_level=LogLevel.DEBUG + ) + logger.activity_summary(3, 1, 2) + out = stream.getvalue() + assert "3" in out + + +class TestPipelineLog: + def test_pipeline_log_default_info(self): + stream = io.StringIO() + logger = ImpressLogger("pipe1", output_stream=stream, use_colors=False) + logger.pipeline_log("step done") + assert "step done" in stream.getvalue() + + def test_pipeline_log_suppressed_below_min(self): + stream = io.StringIO() + logger = ImpressLogger( + "pipe1", output_stream=stream, use_colors=False, min_level=LogLevel.WARNING + ) + logger.pipeline_log("step done", level=LogLevel.INFO) + assert stream.getvalue() == "" + + def test_pipeline_log_debug_level(self): + stream = io.StringIO() + logger = ImpressLogger("pipe1", output_stream=stream, use_colors=False) + logger.pipeline_log("debug step", level=LogLevel.DEBUG) + assert "debug step" in stream.getvalue() + + def test_pipeline_log_includes_pipeline_name_component(self): + stream = io.StringIO() + logger = ImpressLogger("mypipe", output_stream=stream, use_colors=False) + logger.pipeline_log("event") + assert "PIPELINE-MYPIPE" in stream.getvalue() + + +class TestSeparator: + def test_separator_no_title(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream, use_colors=False) + logger.separator() + assert "=" in stream.getvalue() + + def test_separator_with_title(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream, use_colors=False) + logger.separator("HELLO WORLD") + assert "HELLO WORLD" in stream.getvalue() + + def test_separator_ends_with_newline(self): + stream = io.StringIO() + logger = ImpressLogger(output_stream=stream, use_colors=False) + logger.separator() + assert stream.getvalue().endswith("\n") diff --git a/tests/unit/test_manager_core.py b/tests/unit/test_manager_core.py index d35495c..883e912 100644 --- a/tests/unit/test_manager_core.py +++ b/tests/unit/test_manager_core.py @@ -3,8 +3,7 @@ import pytest # Import the classes we're testing -from impress import ImpressBasePipeline, PipelineSetup -from impress import ImpressManager +from impress import ImpressBasePipeline, ImpressManager, PipelineSetup class MockPipeline(ImpressBasePipeline): diff --git a/tests/unit/test_manager_life_cycle.py b/tests/unit/test_manager_life_cycle.py index 9783467..38f7806 100644 --- a/tests/unit/test_manager_life_cycle.py +++ b/tests/unit/test_manager_life_cycle.py @@ -15,6 +15,9 @@ class MockWorkflowEngine: async def create(cls, backend=None): return cls() + async def shutdown(self): + pass + class TestManagerLifecycle: @pytest.mark.asyncio @@ -113,3 +116,18 @@ async def run(self): # Should take at least 0.15 seconds due to slow adaptive function assert end_time - start_time >= 0.15 + + @pytest.mark.asyncio + @patch("impress.impress_manager.WorkflowEngine", MockWorkflowEngine) + async def test_start_exception_still_shuts_down_engine(self, impress_manager): + """WorkflowEngine.shutdown() is called even when a pipeline raises mid-run""" + + class ExplodingPipeline(MockPipeline): + async def run(self): + raise RuntimeError("pipeline exploded") + + await impress_manager.start([ + {"name": "boom", "type": ExplodingPipeline, "config": {}, "kwargs": {}} + ]) + # If shutdown() raises AttributeError, the try/finally fix is broken + assert len(impress_manager.pipeline_tasks) == 0 diff --git a/tests/unit/test_manager_pipeline_submission.py b/tests/unit/test_manager_pipeline_submission.py index e390b93..50999b4 100644 --- a/tests/unit/test_manager_pipeline_submission.py +++ b/tests/unit/test_manager_pipeline_submission.py @@ -1,5 +1,7 @@ from unittest.mock import Mock, patch +import pytest + # Import the classes we're testing from impress import PipelineSetup @@ -64,3 +66,10 @@ def test_submit_multiple_pipelines(self, mock_create_task, impress_manager): assert len(impress_manager.pipeline_tasks) == 2 assert mock_create_task.call_count == 2 + + def test_submit_before_start_raises(self, impress_manager): + """submit_new_pipelines raises RuntimeError when called before start()""" + with pytest.raises(RuntimeError, match="start\\(\\) must be called"): + impress_manager.submit_new_pipelines([ + {"name": "p", "type": MockPipeline, "config": {}, "kwargs": {}} + ]) diff --git a/tests/unit/test_pipeline_base.py b/tests/unit/test_pipeline_base.py new file mode 100644 index 0000000..928ea2d --- /dev/null +++ b/tests/unit/test_pipeline_base.py @@ -0,0 +1,157 @@ +import asyncio + +import pytest + +from impress.pipelines.impress_pipeline import ImpressBasePipeline + + +class MinimalPipeline(ImpressBasePipeline): + """Minimal concrete subclass for testing the base class.""" + + async def run(self): + pass + + def register_pipeline_tasks(self): + pass + + +class TestImpressBasePipelineInit: + def test_name_set(self): + p = MinimalPipeline(name="p1") + assert p.name == "p1" + + def test_flow_defaults_to_none(self): + p = MinimalPipeline(name="p1") + assert p.flow is None + + def test_flow_passed_through(self): + sentinel = object() + p = MinimalPipeline(name="p1", flow=sentinel) + assert p.flow is sentinel + + def test_state_is_empty_dict(self): + p = MinimalPipeline(name="p1") + assert p.state == {} + + def test_kill_parent_false(self): + p = MinimalPipeline(name="p1") + assert p.kill_parent is False + + def test_invoke_adaptive_step_false(self): + p = MinimalPipeline(name="p1") + assert p.invoke_adaptive_step is False + + def test_adaptive_barrier_is_event(self): + p = MinimalPipeline(name="p1") + assert isinstance(p._adaptive_barrier, asyncio.Event) + + def test_incoming_child_pipeline_request_empty(self): + p = MinimalPipeline(name="p1") + assert not p.incoming_child_pipeline_request + + def test_kwargs_stored_in_config(self): + p = MinimalPipeline(name="p1", foo="bar", baz=42) + assert p.config["foo"] == "bar" + assert p.config["baz"] == 42 + + +class TestChildPipelineRequest: + def test_submit_sets_request(self): + p = MinimalPipeline(name="p1") + config = {"name": "child", "type": MinimalPipeline} + p.submit_child_pipeline_request(config) + assert p.incoming_child_pipeline_request == config + + def test_get_returns_config_then_clears(self): + p = MinimalPipeline(name="p1") + config = {"name": "child", "type": MinimalPipeline} + p.submit_child_pipeline_request(config) + + result1 = p.get_child_pipeline_request() + assert result1 == config + + result2 = p.get_child_pipeline_request() + assert result2 is None + + def test_get_returns_none_when_nothing_pending(self): + p = MinimalPipeline(name="p1") + assert p.get_child_pipeline_request() is None + + def test_get_clears_after_retrieval(self): + p = MinimalPipeline(name="p1") + p.submit_child_pipeline_request({"name": "c"}) + p.get_child_pipeline_request() + assert not p.incoming_child_pipeline_request + + +class TestAdaptiveStep: + @pytest.mark.asyncio + async def test_run_adaptive_step_sets_flag(self): + p = MinimalPipeline(name="p1") + # wait=False: flag is set without blocking on the barrier + await p.run_adaptive_step(wait=False) + assert p.invoke_adaptive_step is True + + @pytest.mark.asyncio + async def test_run_adaptive_step_no_wait_does_not_hang(self): + p = MinimalPipeline(name="p1") + # _adaptive_barrier is clear — with wait=False this must return immediately + await asyncio.wait_for(p.run_adaptive_step(wait=False), timeout=1.0) + assert p.invoke_adaptive_step is True + + def test_set_adaptive_flag_true_clears_barrier(self): + p = MinimalPipeline(name="p1") + p._adaptive_barrier.set() + p._set_adaptive_flag(True) + assert p.invoke_adaptive_step is True + assert not p._adaptive_barrier.is_set() + + def test_set_adaptive_flag_false_does_not_touch_barrier(self): + p = MinimalPipeline(name="p1") + p._adaptive_barrier.set() + p._set_adaptive_flag(False) + assert p.invoke_adaptive_step is False + assert p._adaptive_barrier.is_set() # barrier unchanged + + +class TestOptionalMethods: + @pytest.mark.asyncio + async def test_finalize_is_noop(self): + p = MinimalPipeline(name="p1") + result = await p.finalize() + assert result is None + + @pytest.mark.asyncio + async def test_get_scores_map_returns_empty_dict(self): + p = MinimalPipeline(name="p1") + scores = await p.get_scores_map() + assert scores == {} + + def test_get_current_config_has_name_and_type(self): + p = MinimalPipeline(name="p1") + cfg = p.get_current_config_for_next_pipeline() + assert "name" in cfg + assert "type" in cfg + + def test_get_current_config_type_is_class(self): + p = MinimalPipeline(name="p1") + cfg = p.get_current_config_for_next_pipeline() + assert cfg["type"] is MinimalPipeline + + +class TestAbstractMethods: + def test_cannot_instantiate_without_run(self): + class NoRun(ImpressBasePipeline): + def register_pipeline_tasks(self): + pass + + with pytest.raises(TypeError): + NoRun(name="x") + + def test_cannot_instantiate_without_register(self): + class NoRegister(ImpressBasePipeline): + async def run(self): + pass + + with pytest.raises(TypeError): + NoRegister(name="x") diff --git a/tests/unit/test_pipeline_setup.py b/tests/unit/test_pipeline_setup.py new file mode 100644 index 0000000..7212f14 --- /dev/null +++ b/tests/unit/test_pipeline_setup.py @@ -0,0 +1,142 @@ + +import pytest +from pydantic import ValidationError + +from impress import PipelineSetup + +from .test_manager_core import MockPipeline + + +class TestPipelineSetupConstruction: + def test_all_fields(self): + async def fn(p): + pass + + setup = PipelineSetup( + name="my_pipe", + type=MockPipeline, + config={"a": 1}, + kwargs={"b": 2}, + adaptive_fn=fn, + ) + assert setup.name == "my_pipe" + assert setup.type is MockPipeline + assert setup.config == {"a": 1} + assert setup.kwargs == {"b": 2} + assert setup.adaptive_fn is fn + + def test_defaults(self): + setup = PipelineSetup(name="p", type=MockPipeline) + assert setup.config == {} + assert setup.kwargs == {} + assert setup.adaptive_fn is None + + def test_validate_type_rejects_non_subclass(self): + with pytest.raises(ValidationError): + PipelineSetup(name="p", type=str) + + def test_validate_type_rejects_non_type(self): + with pytest.raises(ValidationError): + PipelineSetup(name="p", type="not_a_class") + + def test_validate_type_accepts_subclass(self): + class Sub(MockPipeline): + pass + + setup = PipelineSetup(name="p", type=Sub) + assert setup.type is Sub + + +class TestFromDict: + def test_known_fields_separated(self): + async def fn(p): + pass + + data = { + "name": "p1", + "type": MockPipeline, + "config": {"x": 1}, + "adaptive_fn": fn, + } + setup = PipelineSetup.from_dict(data) + assert setup.name == "p1" + assert setup.type is MockPipeline + assert setup.config == {"x": 1} + assert setup.adaptive_fn is fn + assert setup.kwargs == {} + + def test_extra_keys_go_to_kwargs(self): + data = { + "name": "p1", + "type": MockPipeline, + "foo": "bar", + "baz": 42, + } + setup = PipelineSetup.from_dict(data) + assert setup.kwargs == {"foo": "bar", "baz": 42} + + def test_kwargs_key_in_dict_lands_in_kwargs(self): + # "kwargs" is not a known field, so it ends up nested inside kwargs + data = { + "name": "p1", + "type": MockPipeline, + "kwargs": {"inner": "value"}, + } + setup = PipelineSetup.from_dict(data) + assert setup.kwargs == {"kwargs": {"inner": "value"}} + + def test_minimal_dict(self): + setup = PipelineSetup.from_dict({"name": "p", "type": MockPipeline}) + assert setup.name == "p" + assert setup.config == {} + assert setup.kwargs == {} + assert setup.adaptive_fn is None + + +class TestToDict: + def test_basic_structure(self): + setup = PipelineSetup( + name="p1", + type=MockPipeline, + config={"c": 1}, + ) + d = setup.to_dict() + assert d["name"] == "p1" + assert d["type"] is MockPipeline + assert d["config"] == {"c": 1} + + def test_adaptive_fn_omitted_when_none(self): + setup = PipelineSetup(name="p", type=MockPipeline, adaptive_fn=None) + d = setup.to_dict() + assert "adaptive_fn" not in d + + def test_adaptive_fn_included_when_set(self): + async def fn(p): + pass + + setup = PipelineSetup(name="p", type=MockPipeline, adaptive_fn=fn) + d = setup.to_dict() + assert d["adaptive_fn"] is fn + + def test_kwargs_spread_into_result(self): + setup = PipelineSetup( + name="p", + type=MockPipeline, + kwargs={"foo": "bar", "num": 7}, + ) + d = setup.to_dict() + assert d["foo"] == "bar" + assert d["num"] == 7 + + def test_roundtrip_from_dict(self): + original = { + "name": "rt", + "type": MockPipeline, + "config": {"k": "v"}, + "extra_param": 99, + } + setup = PipelineSetup.from_dict(original) + d = setup.to_dict() + assert d["name"] == "rt" + assert d["type"] is MockPipeline + assert d["extra_param"] == 99 From c93b2b83681d446712d625e675f0420fde71faea Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Wed, 2 Sep 2026 13:13:49 -0500 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20add=20impress=5Fr=20=E2=80=94=20p?= =?UTF-8?q?rotein=20binding=20pipeline=20with=20ROME-A=20fine-tuning=20int?= =?UTF-8?q?egration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combines modern IMPRESS protein_binding (Boltz structure prediction) with ROME-A online ProteinMPNN fine-tuning. ROME hooks live entirely in the adaptive function so the pipeline class stays clean. Includes SLURM scripts, env setup, score analysis plotting script, and run 21736435 report. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DbNxkFCwkCEEkHnN8xGH7Q --- examples/impress_r/delta_env_setup.sh | 262 ++++++ examples/impress_r/delta_gpu_run.sh | 120 +++ examples/impress_r/mpnn.py | 770 ++++++++++++++++++ examples/impress_r/mpnn_train_wrapper.py | 242 ++++++ examples/impress_r/mpnn_wrapper.py | 141 ++++ examples/impress_r/plddt_extract_pipeline.py | 63 ++ examples/impress_r/plot_rome_scores.py | 229 ++++++ examples/impress_r/protein_binding_rome.py | 335 ++++++++ examples/impress_r/rome_global_trend.png | Bin 0 -> 107484 bytes examples/impress_r/rome_pass_distribution.png | Bin 0 -> 46170 bytes .../impress_r/rome_pipeline_trajectories.png | Bin 0 -> 131380 bytes examples/impress_r/run_21736435_report.md | 156 ++++ .../impress_r/run_protein_binding_rome.py | 353 ++++++++ examples/impress_r/scripts/s1_mpnn.sh | 23 + examples/impress_r/scripts/s4_boltz.sh | 65 ++ .../impress_r/scripts/s5_plddt_extract.sh | 20 + 16 files changed, 2779 insertions(+) create mode 100755 examples/impress_r/delta_env_setup.sh create mode 100755 examples/impress_r/delta_gpu_run.sh create mode 100644 examples/impress_r/mpnn.py create mode 100644 examples/impress_r/mpnn_train_wrapper.py create mode 100644 examples/impress_r/mpnn_wrapper.py create mode 100644 examples/impress_r/plddt_extract_pipeline.py create mode 100644 examples/impress_r/plot_rome_scores.py create mode 100644 examples/impress_r/protein_binding_rome.py create mode 100644 examples/impress_r/rome_global_trend.png create mode 100644 examples/impress_r/rome_pass_distribution.png create mode 100644 examples/impress_r/rome_pipeline_trajectories.png create mode 100644 examples/impress_r/run_21736435_report.md create mode 100644 examples/impress_r/run_protein_binding_rome.py create mode 100755 examples/impress_r/scripts/s1_mpnn.sh create mode 100755 examples/impress_r/scripts/s4_boltz.sh create mode 100755 examples/impress_r/scripts/s5_plddt_extract.sh diff --git a/examples/impress_r/delta_env_setup.sh b/examples/impress_r/delta_env_setup.sh new file mode 100755 index 0000000..7870f28 --- /dev/null +++ b/examples/impress_r/delta_env_setup.sh @@ -0,0 +1,262 @@ +#!/bin/bash +# ============================================================================= +# IMPRESS-R (Protein Binding + ROME fine-tuning) environment setup — Delta HPC +# +# Creates a Python 3.11+ venv and installs all dependencies, including ROME-A. +# +# Usage: +# export SCRATCH=/scratch/ +# bash delta_env_setup.sh [--env-dir DIR] [--impress-dir DIR] [--rome-dir DIR] [--python PATH] +# +# Defaults: +# ENV_DIR = /u/$USER/ve/impress +# IMPRESS_DIR = $SCRATCH/$USER/IMPRESS +# ROME_DIR = $SCRATCH/$USER/ROME +# python = auto-detected (python/3.11, cray-python/3.11.7, anaconda3) +# ============================================================================= +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + set -euo pipefail +fi + +# ── Require SCRATCH ─────────────────────────────────────────────────────────── +if [[ -z "${SCRATCH:-}" ]]; then + echo "ERROR: set the SCRATCH env var to your allocation scratch root, e.g.:" + echo " export SCRATCH=/scratch/" + echo " bash delta_env_setup.sh" + exit 1 +fi + +# ── Defaults / arg parsing ──────────────────────────────────────────────────── +ENV_DIR="${ENV_DIR:-/u/${USER}/ve/impress}" +IMPRESS_DIR="${IMPRESS_DIR:-${SCRATCH}/${USER}/IMPRESS}" +ROME_DIR="${ROME_DIR:-${SCRATCH}/${USER}/ROME}" +BASE_PY_OVERRIDE="" + +while [[ $# -gt 0 ]]; do + case $1 in + --env-dir) ENV_DIR="$2"; shift 2 ;; + --impress-dir) IMPRESS_DIR="$2"; shift 2 ;; + --rome-dir) ROME_DIR="$2"; shift 2 ;; + --python) BASE_PY_OVERRIDE="$2"; shift 2 ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done + +PY="${ENV_DIR}/bin/python" +PIP="${ENV_DIR}/bin/pip" + +echo "=================================================================" +echo " ENV_DIR = ${ENV_DIR}" +echo " IMPRESS_DIR = ${IMPRESS_DIR}" +echo " ROME_DIR = ${ROME_DIR}" +echo "=================================================================" + +# ── 1. Create venv ──────────────────────────────────────────────────────────── +echo "" +echo "── Step 1: Creating venv ──" + +_find_python() { + for candidate in python3.12 python3.11 python3 python; do + local p + p=$(command -v "${candidate}" 2>/dev/null) || continue + local ver + ver=$("${p}" -c "import sys; v=sys.version_info; print(v.major*10+v.minor)" 2>/dev/null) || continue + [ "${ver}" -ge 311 ] && echo "${p}" && return 0 + done + return 1 +} + +if [ -n "${BASE_PY_OVERRIDE}" ]; then + BASE_PY="${BASE_PY_OVERRIDE}" + echo "Using Python override: ${BASE_PY}" +else + BASE_PY=$(_find_python || true) + if [ -z "${BASE_PY}" ]; then + echo "python3.11+ not in PATH — trying modules..." + for mod in python/3.12 python/3.11 cray-python/3.11.7 anaconda3; do + module load "${mod}" 2>/dev/null || true + BASE_PY=$(_find_python || true) + [ -n "${BASE_PY}" ] && echo " loaded module: ${mod}" && break + done + fi + if [ -z "${BASE_PY}" ]; then + echo "ERROR: no Python 3.11+ interpreter found." + echo " Pass an explicit interpreter: --python /path/to/python3.11" + echo " Or load a module manually before running this script." + exit 1 + fi +fi +echo "Using Python: ${BASE_PY} ($(${BASE_PY} --version))" + +if [ ! -x "${PY}" ]; then + "${BASE_PY}" -m venv "${ENV_DIR}" +else + echo "venv already exists at ${ENV_DIR}" +fi + +echo "Python: $("${PY}" --version)" + +# ── 2. Bootstrap pip ────────────────────────────────────────────────────────── +echo "" +echo "── Step 2: Bootstrapping pip ──" +"${PY}" -m pip install -q --upgrade pip wheel +"${PIP}" install -q --force-reinstall "setuptools<71" + +# ── 3. radical.asyncflow (PyPI) ────────────────────────────────────────────── +echo "" +echo "── Step 3: radical-asyncflow (PyPI) ──" +"${PIP}" install -q radical-asyncflow + +# ── 4. rhapsody-py (PyPI) ──────────────────────────────────────────────────── +echo "" +echo "── Step 4: rhapsody-py[dragon] (PyPI) ──" +"${PIP}" install -q "rhapsody-py[dragon,telemetry]" + +# ── 5. IMPRESS (local editable) ─────────────────────────────────────────────── +echo "" +echo "── Step 5: IMPRESS (editable) ──" +"${PIP}" install -q -e "${IMPRESS_DIR}" + +# ── 6. ROME (local editable) ───────────────────────────────────────────────── +echo "" +echo "── Step 6: ROME (editable) ──" +if [ -d "${ROME_DIR}" ]; then + "${PIP}" install -q -e "${ROME_DIR}" +else + echo "WARNING: ROME_DIR=${ROME_DIR} not found — skipping ROME install." + echo " Set --rome-dir or clone ROME before running impress_r." +fi + +# ── 7. PyTorch (CUDA 12.1) ─────────────────────────────────────────────────── +echo "" +echo "── Step 7: PyTorch (cu121) ──" +"${PIP}" install -q torch --index-url https://download.pytorch.org/whl/cu121 + +# ── 8. Additional dependencies ─────────────────────────────────────────────── +echo "" +echo "── Step 8: pandas + biopandas ──" +"${PIP}" install -q pandas biopandas + +# ── 9. PyRosetta (via pyrosetta-installer) ─────────────────────────────────── +echo "" +echo "── Step 9: PyRosetta ──" +"${PIP}" install -q pyrosetta-installer +"${PY}" -c "import pyrosetta_installer; pyrosetta_installer.install_pyrosetta()" + +# ── 10. Boltz (structure prediction — separate Python 3.12 conda env) ──────── +echo "" +echo "── Step 10: Boltz (separate conda env) ──" +# boltz 2.x requires numpy<2.0, scipy==1.13.1, etc. — none have Python 3.13 +# wheels, so boltz cannot be installed in the main Python 3.13 IMPRESS venv. +# Create a dedicated Python 3.12 conda env and install boltz there. +# s4_boltz.sh activates BOLTZ_VENV (set in delta_gpu_run.sh) instead of VIRTUAL_ENV. +MINIFORGE="${MINIFORGE:-/scratch/bblj/${USER}/miniforge3}" +BOLTZ_ENV="${BOLTZ_ENV:-${HOME}/ve/boltz}" +# Cache lives in home dir — scratch inode quota can't hold the 45K CCD files. +BOLTZ_CACHE="${BOLTZ_CACHE:-${HOME}/boltz}" +if [ ! -x "${BOLTZ_ENV}/bin/python" ]; then + echo " Creating conda env (Python 3.11) at ${BOLTZ_ENV}" + # Use /tmp for package cache to avoid scratch quota exhaustion. + CONDA_PKGS_DIRS=/tmp/conda_pkgs "${MINIFORGE}/bin/conda" create -p "${BOLTZ_ENV}" python=3.11 -y -q +else + echo " boltz conda env already exists at ${BOLTZ_ENV}" +fi +echo " Installing boltz into ${BOLTZ_ENV}" +"${BOLTZ_ENV}/bin/pip" install -q boltz +# Pre-warm the boltz cache so compute nodes (no internet) find weights ready. +# boltz downloads mols.tar (45K CCD pkl files) + model weights on first run. +# Running predict on the login node (which has internet) pre-populates them. +echo " Pre-warming boltz cache at ${BOLTZ_CACHE}" +mkdir -p "${BOLTZ_CACHE}" +_WARM_FA="$(mktemp /tmp/boltz_warmup_XXXXXX.fasta)" +printf ">warmup|A\nGSSGSSGSS\n>warmup|B\nGSSGSSGSS\n" > "${_WARM_FA}" +BOLTZ_CACHE_DIR="${BOLTZ_CACHE}" "${BOLTZ_ENV}/bin/boltz" predict "${_WARM_FA}" \ + --out_dir "$(mktemp -d /tmp/boltz_warmup_out_XXXXXX)" \ + --cache "${BOLTZ_CACHE}" \ + --override 2>&1 | grep -E "Download|Extracting|Error|error" || true +rm -f "${_WARM_FA}" +echo " Boltz cache pre-warm done (model weights cached at ${BOLTZ_CACHE})" + +# Pre-compute MSAs for all input proteins using the MSA server (login node has internet). +# protein_binding_rome.py s3() reads from BOLTZ_MSA_CACHE and embeds paths in the FASTA so +# compute nodes do not need internet access. Entity 0 = receptor, entity 1 = peptide. +BOLTZ_MSA_CACHE="${BOLTZ_CACHE}/msa_cache" +mkdir -p "${BOLTZ_MSA_CACHE}" +echo " Pre-computing MSAs into ${BOLTZ_MSA_CACHE}" +_scratch="${SCRATCH:-/scratch/bblj/${USER}}" +_base_dir="${IMPRESS_BASE_DIR:-${_scratch}/IMPRESS_inputs}" +_out_dir="${IMPRESS_OUTPUT_DIR:-${_scratch}/IMPRESS_outputs}" +_msa_inputs_dir="${_base_dir}/prod_in" +if [ -d "${_msa_inputs_dir}" ]; then + for _pdb_dir in "${_msa_inputs_dir}"/p*_in; do + for _pdb in "${_pdb_dir}"/*.pdb; do + [ -f "${_pdb}" ] || continue + _stem="$(basename "${_pdb}" .pdb)" + _msa_csv="${BOLTZ_MSA_CACHE}/boltz_results_${_stem}/msa/${_stem}_0.csv" + if [ -f "${_msa_csv}" ]; then + echo " ${_stem}: MSA already cached, skipping" + continue + fi + echo " ${_stem}: generating MSA via server..." + _tmp_fa="$(mktemp /tmp/boltz_msa_XXXXXX.fasta)" + _prior_fa="" + for _cand in "${_out_dir}"/af_pipeline_outputs_multi/*/af/fasta/"${_stem}.fa"; do + [ -f "${_cand}" ] && { _prior_fa="${_cand}"; break; } + done + if [ -n "${_prior_fa}" ]; then + cp "${_prior_fa}" "${_tmp_fa}" + else + printf ">pdz|protein\nGSSGSS\n>pep|protein\nGSSG\n" > "${_tmp_fa}" + fi + BOLTZ_CACHE_DIR="${BOLTZ_CACHE}" "${BOLTZ_ENV}/bin/boltz" predict "${_tmp_fa}" \ + --out_dir "${BOLTZ_MSA_CACHE}" \ + --use_msa_server \ + --cache "${BOLTZ_CACHE}" \ + --output_format pdb \ + --override 2>&1 | grep -E "MSA|Generat|Error|error|skip" || true + rm -f "${_tmp_fa}" + done + done +else + echo " prod_in not found at ${_msa_inputs_dir}, skipping MSA pre-compute" +fi +echo " MSA pre-compute done" + +# ── 11. Verify ─────────────────────────────────────────────────────────────── +echo "" +echo "── Step 11: Verifying installation ──" +_check() { + local label="$1"; shift + if out=$("$@" 2>&1); then + echo " ${label}: OK (${out})" + else + echo " WARNING: ${label} failed" + echo " ${out}" | head -3 + fi +} + +_check "radical.asyncflow" "${PY}" -c "import radical.asyncflow; print(radical.asyncflow.__version__)" +_check "rhapsody-py" "${PY}" -c "import rhapsody; print('ok')" +_check "impress" "${PY}" -c "import impress; print('ok')" +_check "rome" "${PY}" -c "import rome; print('ok')" +_check "torch" "${PY}" -c "import torch; print(torch.__version__)" +_check "pandas" "${PY}" -c "import pandas; print(pandas.__version__)" +BOLTZ_ENV="${BOLTZ_ENV:-${HOME}/ve/boltz}" +_check "boltz" "${BOLTZ_ENV}/bin/python" -c "import boltz; print('ok')" + +echo "" +echo "=================================================================" +echo "Setup complete." +echo "" +echo "Activate with:" +echo " source ${ENV_DIR}/bin/activate" +echo "" +echo "Run the pipeline:" +echo " export SCRATCH=${SCRATCH}" +echo " export SBATCH_ACCOUNT=bblj-delta-gpu" +echo " cd ${IMPRESS_DIR}/examples/impress_r" +echo " sbatch delta_gpu_run.sh" +echo "" +echo "Test (smoke) run:" +echo " IMPRESS_TEST_MODE=1 ROME_TRAINER=dummy sbatch delta_gpu_run.sh" +echo "=================================================================" diff --git a/examples/impress_r/delta_gpu_run.sh b/examples/impress_r/delta_gpu_run.sh new file mode 100755 index 0000000..549c0aa --- /dev/null +++ b/examples/impress_r/delta_gpu_run.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# +# IMPRESS-R (Protein Binding + ROME fine-tuning) — SLURM batch script (Delta HPC / GPU) +# +# Set before calling sbatch (only SBATCH_ACCOUNT and SCRATCH are required): +# export SBATCH_ACCOUNT=-delta-gpu +# export SCRATCH=/scratch/ +# +# Key env vars (all have defaults): +# MPNN_PATH — dauparas/ProteinMPNN checkout (inference AND fine-tune target) +# ROME_MPNN_REPO — same as MPNN_PATH by default +# ROME_TRAINER — mpnn (real fine-tune) | dummy (smoke test, default) +# ROME_MIN_SAMPLES — corpus size before the first training round (default 2) +# ROME_MAX_PASSES — max design passes per pipeline (default 10) +# ROME_FALLBACK — seconds to wait for a training result from Dragon (default 60) +# IMPRESS_BASE_DIR — parent of prod_in/ (input PDB files) +# IMPRESS_OUTPUT_DIR — where af_pipeline_outputs_multi/ is written +# +# Debug flags: +# IMPRESS_TEST_MODE=1 — 2 pipelines, max_passes=1, no child pipelines +# ROME_TRAINER=dummy — skip real fine-tuning (useful with IMPRESS_TEST_MODE) +# +# Example: +# sbatch delta_gpu_run.sh +# IMPRESS_TEST_MODE=1 ROME_TRAINER=dummy sbatch delta_gpu_run.sh +# +#SBATCH --partition=gpuA40x4 +#SBATCH --nodes=1 +#SBATCH --tasks-per-node=1 +#SBATCH --cpus-per-task=16 +#SBATCH --gpus-per-node=4 +#SBATCH --mem=220G +#SBATCH --time=02:00:00 +#SBATCH --job-name=impress_r +#SBATCH --mail-user=mg2347@soe.rutgers.edu +#SBATCH --mail-type=END,FAIL +#SBATCH --output=logs/impress_%j.out +#SBATCH --error=logs/impress_%j.err +# NOTE: logs/ must exist before sbatch is called. Create it once with: +# mkdir -p /logs + +set -e + +# ── Sanity checks ───────────────────────────────────────────────────────────── +if [ -z "${SBATCH_ACCOUNT:-}${SLURM_JOB_ACCOUNT:-}" ]; then + echo "WARNING: SBATCH_ACCOUNT is not set — job may be charged to default account." +fi +echo "Account: ${SLURM_JOB_ACCOUNT:-unknown}" + +if [ -z "${SCRATCH:-}" ]; then + echo "ERROR: SCRATCH is not set." + echo " export SCRATCH=/scratch/ && sbatch delta_gpu_run.sh" + exit 1 +fi + +# ── System library paths (Delta-specific, required by Dragon) ───────────────── +export CUDA_HOME=/opt/nvidia/hpc_sdk/Linux_x86_64/25.3/cuda/12.8 +export MPI_LIB=/opt/cray/pe/mpich/8.1.32/ofi/gnu/11.2/lib-abi-mpich +export FAB_LIB=/opt/cray/libfabric/1.22.0/lib64 +export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH:-} + +# ── Environment ─────────────────────────────────────────────────────────────── +IMPRESS_VENV="${IMPRESS_VENV:-${HOME}/ve/impress}" +unset SLURM_EXPORT_ENV +source "${IMPRESS_VENV}/bin/activate" +dragon-config add --ofi-runtime-lib="${FAB_LIB}" + +# ── Tool paths ──────────────────────────────────────────────────────────────── +# ProteinMPNN checkout — used for inference (mpnn_wrapper.py) and fine-tuning (ROME). +export MPNN_PATH="${MPNN_PATH:-${SCRATCH}/${USER}/ProteinMPNN}" +# ROME-A fine-tunes the same checkout and publishes weights back into it. +export ROME_MPNN_REPO="${ROME_MPNN_REPO:-${MPNN_PATH}}" + +# Boltz — separate Python <=3.12 env (numpy<2.0 etc.) +export BOLTZ_VENV="${BOLTZ_VENV:-${HOME}/ve/boltz}" +export BOLTZ_CACHE_DIR="${BOLTZ_CACHE_DIR:-${HOME}/boltz}" +mkdir -p "${BOLTZ_CACHE_DIR}" + +# ── IMPRESS paths ───────────────────────────────────────────────────────────── +export IMPRESS_SCRIPTS_DIR="${IMPRESS_SCRIPTS_DIR:-${SCRATCH}/${USER}/IMPRESS/examples/impress_r}" +export IMPRESS_BASE_DIR="${IMPRESS_BASE_DIR:-${SCRATCH}/${USER}/IMPRESS_inputs}" +export IMPRESS_OUTPUT_DIR="${IMPRESS_OUTPUT_DIR:-${SCRATCH}/${USER}/IMPRESS_outputs}" + +# ── ROME-A settings ─────────────────────────────────────────────────────────── +# ROME_TRAINER=mpnn → real ProteinMPNN fine-tune (needs ROME_MPNN_REPO on disk) +# ROME_TRAINER=dummy → smoke test (no GPU/torch needed for training) +export ROME_TRAINER="${ROME_TRAINER:-mpnn}" +export ROME_MIN_SAMPLES="${ROME_MIN_SAMPLES:-2}" +export ROME_MAX_PASSES="${ROME_MAX_PASSES:-10}" +export ROME_FALLBACK="${ROME_FALLBACK:-60}" + +# ── Test / debug flags ──────────────────────────────────────────────────────── +export IMPRESS_TEST_MODE="${IMPRESS_TEST_MODE:-0}" + +echo "MPNN_PATH: ${MPNN_PATH}" +echo "ROME_MPNN_REPO: ${ROME_MPNN_REPO}" +echo "ROME_TRAINER: ${ROME_TRAINER}" +echo "ROME_MIN_SAMPLES: ${ROME_MIN_SAMPLES}" +echo "IMPRESS_BASE_DIR: ${IMPRESS_BASE_DIR}" +echo "IMPRESS_OUTPUT_DIR:${IMPRESS_OUTPUT_DIR}" +echo "TEST_MODE: ${IMPRESS_TEST_MODE}" + +# ── Working directory ───────────────────────────────────────────────────────── +WORKDIR="${IMPRESS_SCRIPTS_DIR}" +cd "${WORKDIR}" +mkdir -p logs + +# ── Run ─────────────────────────────────────────────────────────────────────── +if [ "${SLURM_NNODES:-1}" -gt 1 ]; then + DRAGON_MODE="-m" +else + DRAGON_MODE="-s" +fi + +rm -f ddict_orc* + +echo "Running: dragon ${DRAGON_MODE} run_protein_binding_rome.py (nodes=${SLURM_NNODES:-1})" +dragon ${DRAGON_MODE} run_protein_binding_rome.py + +echo "=== IMPRESS-R pipeline done: $(date) ===" diff --git a/examples/impress_r/mpnn.py b/examples/impress_r/mpnn.py new file mode 100644 index 0000000..606af60 --- /dev/null +++ b/examples/impress_r/mpnn.py @@ -0,0 +1,770 @@ +"""ProteinMPNN trainer task — the IMPRESS-R half of ROME-A's trainers. + +IMPRESS runs backbone -> ProteinMPNN -> structure prediction -> pLDDT/pTM/pAE +-> keep/fallback/migrate/drop, and it is open loop: every campaign improves the +designs, never the model. IMPRESS-R closes that loop by fine-tuning ProteinMPNN +on the campaign's own high-confidence designs. + +**Which ProteinMPNN.** IMPRESS runs the original ``dauparas/ProteinMPNN``: its +``mpnn_wrapper.py`` shells out to ``protein_mpnn_run.py`` with the original CLI +and helper scripts, and its setup clones that repo directly. So this trainer +targets the *same* implementation — it fine-tunes the original model and writes +a checkpoint in the original ``{"model_state_dict": ...}`` format that +``protein_mpnn_run.py`` loads. A foundry / re-implementation checkpoint would +not load into what IMPRESS runs; that was the previous version's mistake. + +(PyRosetta is in the IMPRESS stack too, but for FastRelax and pLDDT extraction, +not for sequence design — the design model is vanilla ProteinMPNN.) + +**Dimers, not monomers.** An IMPRESS protein-binding design is a *complex*: a +designed chain (``A``) plus a fixed target peptide (``B``). Fine-tuning has to +respect that — the model should learn to design chain A *in the context of* the +peptide, scoring sequence recovery on the designed chain only. That is exactly +what ProteinMPNN's chain mask expresses, and :func:`build_chain_designation` +sets it up: designed chains are predicted, context chains are visible but not +scored. + +This trainer is an IMPRESS-R *integration*, not framework core — ROME-A is +workflow-agnostic — so it lives with the example, beside the inference wrapper +(``mpnn_wrapper.py``) it complements. Import it from here:: + + from examples.impress_r.mpnn import ProteinMPNNTrainer, ProteinMPNNConfig + +**Runs as a command, not a function.** Like IMPRESS — which submits +``mpnn_wrapper.py`` as a shell command rather than calling ProteinMPNN in the +campaign process — the training manager submits this round as an *executable +task*: :meth:`ProteinMPNNTrainer.as_command` stages the structures, writes a +self-contained job spec, and returns ``python mpnn_train_wrapper.py --job +``. The fine-tune therefore runs in its own process on its own GPU, +and that process exits when the round finishes, so its VRAM is released with it. + +The training loop itself lives in ``mpnn_train_wrapper.run_round`` (the sibling +script) — one dragon-free copy, so the standalone script and the in-process path +(a direct :meth:`train` call, used by the tests) share it. It mirrors +``training/training.py``'s inner loop using the repo's own ``featurize``, +``loss_smoothed``, ``NoamOpt`` and training ``ProteinMPNN``, verified end to end +against a real checkout and the public ``v_48_020`` weights — the loss runs on +the designed chain only and the checkpoint reloads into ``protein_mpnn_run.py``. +``config.train_func`` substitutes your own loop (and keeps it in-process). See +``docs/proteinmpnn_training.md``. +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple + +from rome.train.base import TrainTask + + +def _load_run_round(): + """Import ``run_round`` from the sibling ``mpnn_train_wrapper``. + + Works whether this module was imported as ``examples.impress_r.mpnn`` (the + test/package path) or as a bare ``mpnn`` (a script run from inside the + example directory): the wrapper sits next to this file, so its directory is + put on ``sys.path`` and it is imported by name. Lazy — only the in-process + :meth:`ProteinMPNNTrainer.train` path needs it; the command path never + imports the wrapper here, it runs it as a subprocess. + """ + here = os.path.dirname(os.path.abspath(__file__)) + if here not in sys.path: + sys.path.insert(0, here) + from mpnn_train_wrapper import run_round # type: ignore + + return run_round + + +#: ProteinMPNN's amino-acid alphabet (index -> letter). Position 20 is ``X``. +MPNN_ALPHABET = "ACDEFGHIKLMNPQRSTVWYX" + +#: A corpus record must carry ``path`` — the structure file whose designed chain +#: IS the training label. ``sequence`` is optional (ProteinMPNN reads the label +#: out of the structure); it is kept for the manifest and for sizing. +REQUIRED_FIELDS = ("path",) + +#: The IMPRESS protein-binding layout: chain A is the designed binder, chain B +#: the fixed target peptide. Designed chains are scored; context chains are +#: visible to the model but not scored. +DEFAULT_DESIGN_CHAINS: Tuple[str, ...] = ("A",) +DEFAULT_CONTEXT_CHAINS: Tuple[str, ...] = ("B",) + +#: Architecture of the public ``v_48_020`` weights, which is what IMPRESS runs. +#: ``protein_mpnn_run.py`` hardcodes hidden_dim=128 and 3+3 layers and reads +#: ``k_neighbors`` back from the checkpoint's ``num_edges`` — so a fine-tune must +#: keep these or the published checkpoint will not load. +DEFAULT_NUM_NEIGHBORS = 48 +DEFAULT_HIDDEN_DIM = 128 +DEFAULT_NUM_LAYERS = 3 +DEFAULT_BACKBONE_NOISE = 0.2 + + +# --------------------------------------------------------------------------- +# Pure-Python helpers (no torch): chain designation, staging, checkpoint format +# --------------------------------------------------------------------------- + +def pdb_chain_ids(path: str) -> List[str]: + """Chain IDs present in a PDB, in first-seen order — column 22 of ATOM/HETATM. + + Plain text parsing so a campaign's monomer-vs-complex shape can be checked + (and a bad chain designation caught) without importing a structure library. + """ + seen: List[str] = [] + try: + with open(path) as fd: + for line in fd: + if line.startswith(("ATOM", "HETATM")) and len(line) >= 22: + chain = line[21] + if chain not in seen: + seen.append(chain) + except OSError: + return [] + return seen + + +def build_chain_designation( + records: List[Dict[str, Any]], + *, + design_chains: Sequence[str] = DEFAULT_DESIGN_CHAINS, + context_chains: Sequence[str] = DEFAULT_CONTEXT_CHAINS, + chains_func: Optional[Callable[[Dict[str, Any], List[str]], Tuple[List[str], List[str]]]] = None, +) -> Dict[str, Tuple[List[str], List[str]]]: + """Map each design to ``(designed_chains, context_chains)``. + + This is the dimer fix. ProteinMPNN's ``tied_featurize`` takes exactly this + mapping and builds the chain mask from it: designed chains are predicted and + scored, context chains are visible to the model but excluded from the loss. + For an IMPRESS binder that means "learn chain A given chain B", which is the + thing the campaign is actually optimising. + + A record may override the defaults per structure with a ``design_chains`` / + ``context_chains`` field, or a ``chains_func(record, present_chains)`` may + decide from the structure itself. Chains named but absent from the file are + dropped with the designation still valid for whatever remains; a design with + no designable chain present is an error, caught in + :meth:`ProteinMPNNTrainer.validate`. + + Returns ``{design_name: (designed, context)}`` keyed by the staged structure + name (see :func:`stage_structures`), which is what ``tied_featurize`` keys on. + """ + out: Dict[str, Tuple[List[str], List[str]]] = {} + for index, record in enumerate(records): + name = _design_name(record, index) + present = pdb_chain_ids(record["path"]) if record.get("path") else [] + if chains_func is not None: + designed, context = chains_func(record, present) + else: + designed = list(record.get("design_chains", design_chains)) + context = list(record.get("context_chains", context_chains)) + if present: + designed = [c for c in designed if c in present] + context = [c for c in context if c in present] + out[name] = (designed, context) + return out + + +def _design_name(record: Dict[str, Any], index: int) -> str: + """Stable, filesystem-safe name for a design; also its key in the chain map.""" + raw = str(record.get("uid") or record.get("design_id") + or os.path.splitext(os.path.basename(record.get("path", "")))[0] + or f"design_{index}") + return "".join(c if (c.isalnum() or c in "-_") else "_" for c in raw) + + +def stage_structures(records: List[Dict[str, Any]], staging_dir: str) -> Dict[str, str]: + """Copy each design's structure into one directory under a unique name. + + ProteinMPNN parses a *folder* of PDBs, and a campaign's paths collide on + basename (every pipeline writes ``{target}.pdb``) and get overwritten pass to + pass. Staging copies each file under its unique design name, so the training + set is a stable snapshot rather than a set of paths whose contents move. + + Returns ``{design_name: staged_path}``. + """ + os.makedirs(staging_dir, exist_ok=True) + staged: Dict[str, str] = {} + for index, record in enumerate(records): + name = _design_name(record, index) + dst = os.path.join(staging_dir, f"{name}.pdb") + shutil.copyfile(record["path"], dst) + staged[name] = dst + return staged + + +def original_checkpoint( + model_state_dict: Any, *, num_edges: int = DEFAULT_NUM_NEIGHBORS, + noise_level: float = DEFAULT_BACKBONE_NOISE, **extra: Any, +) -> Dict[str, Any]: + """The checkpoint dict ``protein_mpnn_run.py`` loads. + + It reads ``checkpoint['model_state_dict']`` and ``checkpoint['num_edges']`` + (used as ``k_neighbors`` when constructing the model), so both are required + for the published weights to load at all. ``noise_level`` is carried for + parity with the original trainer's saves. + """ + ckpt = {"model_state_dict": model_state_dict, + "num_edges": int(num_edges), "noise_level": float(noise_level)} + ckpt.update(extra) + return ckpt + + +def published_weights_path(config: "ProteinMPNNConfig", output_dir: str) -> str: + """Where the round's weights go so IMPRESS's next pass picks them up. + + ``mpnn_wrapper.py`` never passes ``--path_to_model_weights``, so + ``protein_mpnn_run.py`` loads ``{mpnn_repo}/vanilla_model_weights/{model_name}.pt`` + by default. With ``publish_into_repo`` set (and ``mpnn_repo`` known) the + checkpoint is written *there*, replacing the weights the campaign runs with; + otherwise it lands in the round's ``output_dir`` and the integration is + responsible for pointing MPNN at it (e.g. patch the wrapper to pass a path). + """ + if config.publish_into_repo and config.mpnn_repo: + return os.path.join(config.mpnn_repo, "vanilla_model_weights", + f"{config.model_name}.pt") + return os.path.join(output_dir, f"{config.model_name}.pt") + + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- + +@dataclass +class ProteinMPNNConfig: + """Configuration for fine-tuning the original ProteinMPNN on campaign designs. + + Defaults describe a short mid-campaign round that starts from the public + weights and nudges them, not a from-scratch training run. + + Parameters + ---------- + mpnn_repo : Optional[str] + Path to the ``dauparas/ProteinMPNN`` checkout — the same directory + IMPRESS passes as ``-mpnn``. The trainer imports ``protein_mpnn_utils`` + from here, and (with ``publish_into_repo``) writes the new weights back + into its ``vanilla_model_weights/``. + initial_weights : Optional[str] + Checkpoint the first round starts from, in original format — normally + ``{mpnn_repo}/vanilla_model_weights/v_48_020.pt``. Later rounds resume + from whatever the previous round published (passed as ``model_path``). + model_name : str + Basename of the published weights (no extension). Must match the + ``--model_name`` IMPRESS runs with; the default ``v_48_020`` is + ``protein_mpnn_run.py``'s default. + design_chains, context_chains : sequence of str + Default chain designation. For IMPRESS binders, ``("A",)`` designed and + ``("B",)`` context. Overridable per record or via ``chains_func``. + chains_func : optional callable + ``(record, present_chains) -> (designed, context)`` to decide chains per + structure. Overrides the defaults when set. + num_neighbors, hidden_dim, num_layers, backbone_noise, dropout : ... + Architecture. Must match the weights being fine-tuned — the public + ``v_48_*`` weights are ``num_neighbors`` in {48}, hidden_dim 128, 3+3 + layers. ``protein_mpnn_run.py`` reads ``num_neighbors`` back from the + checkpoint, so a mismatch will not load. + max_epochs, batch_tokens, max_protein_length : int + Round length and batching. ``batch_tokens`` caps residues per batch + (ProteinMPNN uses ~10000). A short round is a few epochs. + learning_rate_factor, warmup_steps, label_smoothing, gradient_norm : ... + Noam schedule + label-smoothed NLL, as the original trainer uses. + publish_into_repo : bool + Write the new weights into ``{mpnn_repo}/vanilla_model_weights/`` so the + campaign's next pass runs them with no wrapper change. Off by default: + it mutates the shared repo, which a workflow should opt into knowingly. + seed, num_workers, device : ... + Reproducibility / loader / placement. + train_func : optional callable + ``(manifest_path, output_dir, config) -> checkpoint_path`` escape hatch, + bypassing the built-in loop. The recommended path until the loop is + validated on your checkout, and the way the examples stay runnable. + """ + + mpnn_repo: Optional[str] = None + initial_weights: Optional[str] = None + model_name: str = "v_48_020" + dataset_name: str = "impress_r" + + design_chains: Sequence[str] = DEFAULT_DESIGN_CHAINS + context_chains: Sequence[str] = DEFAULT_CONTEXT_CHAINS + chains_func: Optional[Callable[..., Tuple[List[str], List[str]]]] = None + + num_neighbors: int = DEFAULT_NUM_NEIGHBORS + hidden_dim: int = DEFAULT_HIDDEN_DIM + num_layers: int = DEFAULT_NUM_LAYERS + backbone_noise: float = DEFAULT_BACKBONE_NOISE + dropout: float = 0.1 + + max_epochs: int = 3 + batch_tokens: int = 10000 + max_protein_length: int = 2000 + + learning_rate_factor: float = 2.0 + warmup_steps: int = 4000 + label_smoothing: float = 0.1 + gradient_norm: Optional[float] = 1.0 + + publish_into_repo: bool = False + seed: int = 0 + num_workers: int = 4 + device: str = "cuda" + + manifest_dir: Optional[str] = None + train_func: Optional[Callable[..., str]] = None + #: The wrapper script the round is submitted as a command to run. Defaults to + #: the sibling ``examples/impress_r/mpnn_train_wrapper.py``; override to point + #: at a copy staged elsewhere on the cluster (as IMPRESS points ``-mpnn`` at + #: its own checkout). + train_script: Optional[str] = None + + def validate(self) -> None: + if not self.design_chains and self.chains_func is None: + raise ValueError( + "at least one design chain is required (design_chains is empty " + "and no chains_func was given)" + ) + if self.train_func is None and not self.mpnn_repo: + raise ValueError( + "mpnn_repo must point at a dauparas/ProteinMPNN checkout for the " + "built-in trainer; set config.train_func to use your own loop " + "instead. See docs/proteinmpnn_training.md." + ) + + +# --------------------------------------------------------------------------- +# The training task +# --------------------------------------------------------------------------- + +class ProteinMPNNTrainer(TrainTask): + """Fine-tunes the original ProteinMPNN on the campaign's confident designs. + + Returns the checkpoint file (original format), which the training manager + publishes and — with ``publish_into_repo`` — drops straight into the repo's + weights directory so IMPRESS's next pass runs it. + + Set ``config.train_func`` to bypass the built-in loop with your own + ``(manifest_path, output_dir, config) -> checkpoint_path``. Recommended + until the built-in loop is validated on your ProteinMPNN checkout. + """ + + #: Records are plain dicts of paths and scores, not a HuggingFace dataset. + wants_hf_dataset = False + + def __init__( + self, + config: Optional[ProteinMPNNConfig] = None, + *, + gpus: Optional[int] = None, + nodes: Optional[int] = None, + name: Optional[str] = None, + ): + config = config or ProteinMPNNConfig() + config.validate() + super().__init__( + gpus=gpus if gpus is not None else 1, + nodes=nodes if nodes is not None else 1, + name=name or "proteinmpnn", + ) + self.config = config + + # -- pre-flight --------------------------------------------------------- + + def validate(self, dataset: Any) -> None: + super().validate(dataset) + cfg = self.config + for index, record in enumerate(dataset): + record = record or {} + missing = [f for f in REQUIRED_FIELDS if f not in record] + if missing: + raise ValueError( + f"ProteinMPNN training needs {', '.join(REQUIRED_FIELDS)} on " + f"every record; corpus record {index} is missing " + f"{', '.join(missing)}. 'path' must point at the structure " + "whose designed chain is the training label (for IMPRESS-R, " + "the prediction of the designed sequence) — see " + "docs/proteinmpnn_training.md." + ) + # A design with no designable chain present in the file trains + # nothing and would produce an empty loss mask; catch it here rather + # than on the GPU. + present = pdb_chain_ids(record["path"]) if os.path.exists(record["path"]) else None + if present is not None: + if cfg.chains_func is not None: + designed, _ = cfg.chains_func(record, present) + else: + designed = list(record.get("design_chains", cfg.design_chains)) + if present and not any(c in present for c in designed): + raise ValueError( + f"corpus record {index} ({record['path']}): none of the " + f"designed chains {list(designed)} are present in the " + f"structure (chains: {present}). For an IMPRESS binder the " + "designed chain is 'A' and the peptide 'B'." + ) + + # -- corpus materialization (testable, no torch) ------------------------ + + def write_manifest(self, records: List[Dict[str, Any]], output_dir: str) -> str: + """Write the round's training manifest (parquet); returns its path. + + The audit trail for "what did this round train on": one row per design + with its staged structure, chain designation, and scores. Public and + side-effect-light so a workflow can inspect a round before trusting it. + """ + import pandas as pd + + cfg = self.config + manifest_dir = cfg.manifest_dir or os.path.join(output_dir, "manifest") + os.makedirs(manifest_dir, exist_ok=True) + + designation = build_chain_designation( + records, design_chains=cfg.design_chains, + context_chains=cfg.context_chains, chains_func=cfg.chains_func, + ) + rows = [] + for index, record in enumerate(records): + name = _design_name(record, index) + designed, context = designation[name] + rows.append({ + "design_id": name, + "path": os.path.abspath(record["path"]), + "designed_chains": ",".join(designed), + "context_chains": ",".join(context), + "sequence": record.get("sequence") or "", + "backbone_id": record.get("backbone_id"), + "pLDDT": record.get("pLDDT"), + "pTM": record.get("pTM"), + "pAE": record.get("pAE"), + "produced_under_version": record.get("model_version"), + }) + path = os.path.join(manifest_dir, "train_manifest.parquet") + pd.DataFrame(rows).to_parquet(path) + return path + + # -- the round: prepare a job, run it as a command ---------------------- + + def as_command(self, dataset: Any, output_dir: str, + **kwargs: Any) -> Optional[Tuple[str, str]]: + """Submit this round as a shell command, IMPRESS-style. + + Stages the round's structures, writes a self-contained job spec, and + returns ``(command, checkpoint_path)`` where ``command`` runs + ``mpnn_train_wrapper.py`` on that spec. The training manager runs it as an + *executable task*, so the fine-tune is a separate process on its own GPU + rather than a function in the manager's address space. + + Returns ``None`` when ``config.train_func`` is set — a custom loop is + Python, not a command, so it runs in-process through :meth:`train`. + """ + if self.config.train_func is not None: + return None + records = list(dataset) + self.write_manifest(records, output_dir) + job, target = self._build_job(records, output_dir, **kwargs) + job_path = os.path.join(output_dir, "train_job.json") + with open(job_path, "w") as fd: + json.dump(job, fd, indent=2) + + script = self.config.train_script or os.path.join( + os.path.dirname(os.path.abspath(__file__)), "mpnn_train_wrapper.py" + ) + command = f"{sys.executable} {script} --job {job_path}" + return command, target + + def train(self, dataset: Any, output_dir: str, **kwargs: Any) -> str: + """Fine-tune ProteinMPNN in-process; return the checkpoint. + + This is the direct/in-process path — the training manager normally goes + through :meth:`as_command` instead. Kept because a workflow (or a test) + may want to run a round synchronously, and because ``config.train_func`` + plugs in here. It shares the exact loop the command runs, via + ``mpnn_train_wrapper.run_round``. + + ``kwargs`` carries ``model_version`` from the training manager and + ``model_path`` when a previous round published one. + """ + records = list(dataset) + manifest_path = self.write_manifest(records, output_dir) + + if self.config.train_func is not None: + return self.config.train_func(manifest_path, output_dir, self.config) \ + or output_dir + + run_round = _load_run_round() + job, _target = self._build_job(records, output_dir, **kwargs) + return run_round(job) + + def _build_job(self, records: List[Dict[str, Any]], output_dir: str, + **kwargs: Any) -> Tuple[Dict[str, Any], str]: + """Stage structures and assemble the job spec the wrapper consumes. + + Staging takes a snapshot of each design's structure under a unique name + (a campaign overwrites ``{target}.pdb`` pass to pass), and the chain + designation is resolved here — on the manager side, where the corpus and + the config live — so the wrapper only has to parse and train. Returns + ``(job, target_weights_path)``; see ``mpnn_train_wrapper`` for + the spec. + """ + cfg = self.config + staged = stage_structures(records, os.path.join(output_dir, "structures")) + designation = build_chain_designation( + records, design_chains=cfg.design_chains, + context_chains=cfg.context_chains, chains_func=cfg.chains_func, + ) + designs = [] + for index, record in enumerate(records): + name = _design_name(record, index) + designed, context = designation[name] + designs.append({ + "name": name, + "path": os.path.abspath(staged[name]), + "designed_chains": designed, + "context_chains": context, + }) + + target = os.path.abspath(published_weights_path(cfg, output_dir)) + job = { + "mpnn_repo": cfg.mpnn_repo, + "resume_from": kwargs.get("model_path") or cfg.initial_weights, + "target_weights": target, + "output_dir": os.path.abspath(output_dir), + "designs": designs, + "hyperparams": { + "hidden_dim": cfg.hidden_dim, + "num_layers": cfg.num_layers, + "num_neighbors": cfg.num_neighbors, + "backbone_noise": cfg.backbone_noise, + "dropout": cfg.dropout, + "max_epochs": cfg.max_epochs, + "batch_tokens": cfg.batch_tokens, + "max_protein_length": cfg.max_protein_length, + "learning_rate_factor": cfg.learning_rate_factor, + "warmup_steps": cfg.warmup_steps, + "label_smoothing": cfg.label_smoothing, + "gradient_norm": cfg.gradient_norm, + "seed": cfg.seed, + "device": cfg.device, + "model_name": cfg.model_name, + }, + } + return job, target + + +# --------------------------------------------------------------------------- +# Corpus selection — orthogonal to the trainer, used by DataConfig +# --------------------------------------------------------------------------- + +def impress_corpus_filter( + min_pLDDT: float = 80.0, + min_pTM: float = 0.8, + max_pAE: float = 5.0, +) -> Callable[[Dict[str, Any]], bool]: + """Build the IMPRESS admission predicate for :class:`~rome.data.DataConfig`. + + IMPRESS-R only trains on designs the campaign is confident in, and these are + the thresholds IMPRESS already uses to decide a design is worth keeping:: + + DataConfig(min_samples=24, filter_func=impress_corpus_filter()) + + .. warning:: + + **These defaults are known to be too permissive, and are kept only because + a correct replacement cannot be derived yet.** Measured against a real + PDZ campaign they admit 83% of records: ``pLDDT >= 80`` alone admits 100%, + because everything reaching the score CSVs has already cleared IMPRESS's + own keep/drop rule — the filter is being applied downstream of itself. + + That campaign was run with **Boltz**, while the branch ROME-A targets + (``archive/ipdps_pdz_usecase``) runs **AlphaFold2-multimer**, and the two + predictors do not share a confidence scale. Prefer :func:`percentile_sampler`, + which needs no scale; see ``docs/impress.md``. + """ + + def _passes(record: Dict[str, Any]) -> bool: + plddt = record.get("pLDDT", record.get("score")) + if plddt is None or plddt < min_pLDDT: + return False + if record.get("pTM", min_pTM) < min_pTM: + return False + if record.get("pAE", max_pAE) > max_pAE: + return False + return True + + return _passes + + +def score_percentiles( + records: Sequence[Dict[str, Any]], + keys: Sequence[str] = ("pLDDT", "pTM", "pAE"), +) -> Dict[str, Dict[str, float]]: + """Summarise the score distribution a campaign has produced so far. + + Calibration data, gathered from the run itself. Every confidence threshold + in IMPRESS-R is predictor-specific — AlphaFold2-multimer and Boltz do not + share a scale — so the only safe way to set one is to look at what *this* + campaign is producing:: + + from examples.impress_r.mpnn import score_percentiles + print(score_percentiles(manager.data.get_records())) + + Returns ``{key: {"n", "min", "p10", "p25", "median", "p75", "p90", "max"}}``, + skipping keys no record carries. + """ + out: Dict[str, Dict[str, float]] = {} + for key in keys: + values = sorted( + float(r[key]) for r in records + if r.get(key) is not None and _is_number(r[key]) + ) + if not values: + continue + + def q(p: float) -> float: + return values[min(len(values) - 1, int(p * len(values)))] + + out[key] = { + "n": float(len(values)), "min": values[0], "p10": q(0.10), + "p25": q(0.25), "median": q(0.50), "p75": q(0.75), + "p90": q(0.90), "max": values[-1], + } + return out + + +def _is_number(value: Any) -> bool: + try: + float(value) + except (TypeError, ValueError): + return False + return True + + +#: Default ranking for the PDZ binder case: interface pAE down, pTM up. pLDDT is +#: deliberately absent — in a measured campaign it never fell below 88, so it +#: separates almost nothing. +DEFAULT_RANK_BY = {"pAE": "low", "pTM": "high"} + + +def percentile_sampler( + fraction: float = 0.33, + *, + rank_by: Optional[Dict[str, str]] = None, + min_shard: int = 8, + on_summary: Optional[Callable[[Dict[str, Any]], None]] = None, +) -> Callable[[List[Dict[str, Any]]], List[Dict[str, Any]]]: + """Select the best ``fraction`` of the corpus, calibrating as it goes. + + Use this instead of tuning :func:`impress_corpus_filter`'s thresholds:: + + DataConfig(min_samples=24, sample_func=percentile_sampler(0.33)) + + **Why a fraction rather than a cutoff.** A threshold like ``pTM >= 0.90`` + is a claim about a specific predictor's confidence scale, and IMPRESS + campaigns have been run on both AlphaFold2-multimer and Boltz, which do not + share one. A fraction says "the best third of what this campaign has + produced", which needs no scale and calibrates itself on the fly — including + on the first round, before anyone has seen the distribution. + + It also sidesteps the trap that makes IMPRESS's own keep/drop thresholds + useless here: everything in the corpus already cleared them, so reusing them + as an admission filter selects nothing. + + Ranking is by **average rank across ``rank_by``**, not by a weighted sum of + raw values. Rank-averaging is non-parametric, so pTM (0–1) and pAE (Å, open + ended) contribute equally without needing to be normalised, and it is + unaffected by either metric's outliers. + + Parameters + ---------- + fraction : float + Portion of the corpus to keep, in (0, 1]. + rank_by : Optional[Dict[str, str]] + Record field -> ``'high'`` (higher is better) or ``'low'``. Defaults to + :data:`DEFAULT_RANK_BY`. Fields absent from every record are ignored, so + a corpus carrying only some of them still ranks. + min_shard : int + Never return fewer than this many records — a strict fraction of a small + early corpus can otherwise produce a shard too small to train on. Capped + at the corpus size. + on_summary : Optional[Callable[[dict], None]] + Called with ``{"corpus", "selected", "ranked_by", "percentiles", + "cutoffs"}`` each time a shard is built. Pass ``print`` or a logger to + watch the campaign's distribution move, and to read off the thresholds + an equivalent fixed filter would have used. + """ + if not 0.0 < fraction <= 1.0: + raise ValueError(f"fraction must be in (0, 1], got {fraction}") + directions = dict(rank_by if rank_by is not None else DEFAULT_RANK_BY) + for key, direction in directions.items(): + if direction not in ("high", "low"): + raise ValueError( + f"rank_by[{key!r}] must be 'high' or 'low', got {direction!r}" + ) + + def _sample(records: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + records = list(records) + if not records: + return records + + usable = { + key: direction for key, direction in directions.items() + if any(_is_number(r.get(key)) for r in records) + } + if not usable: + # Nothing to rank on: keep the corpus rather than silently + # returning an arbitrary slice of it. + return records + + # Average rank across metrics. Missing values sort last within their + # metric, so a partially-scored record is penalised but not dropped. + total = {id(r): 0.0 for r in records} + for key, direction in usable.items(): + ordered = sorted( + records, + key=lambda r: ( + not _is_number(r.get(key)), + -float(r[key]) if _is_number(r.get(key)) and direction == "high" + else (float(r[key]) if _is_number(r.get(key)) else 0.0), + ), + ) + for position, record in enumerate(ordered): + total[id(record)] += position + + ranked = sorted(records, key=lambda r: (total[id(r)], str(r.get("uid", "")))) + keep = max(min(min_shard, len(records)), int(round(len(records) * fraction))) + selected = ranked[:keep] + + if on_summary is not None: + cutoffs = {} + for key, direction in usable.items(): + values = [float(r[key]) for r in selected if _is_number(r.get(key))] + if values: + cutoffs[key] = min(values) if direction == "high" else max(values) + on_summary({ + "corpus": len(records), + "selected": len(selected), + "ranked_by": usable, + "percentiles": score_percentiles(records, tuple(usable)), + "cutoffs": cutoffs, + }) + return selected + + return _sample + + +__all__ = [ + "ProteinMPNNConfig", + "ProteinMPNNTrainer", + "build_chain_designation", + "stage_structures", + "pdb_chain_ids", + "original_checkpoint", + "published_weights_path", + "impress_corpus_filter", + "percentile_sampler", + "score_percentiles", + "DEFAULT_RANK_BY", + "DEFAULT_DESIGN_CHAINS", + "DEFAULT_CONTEXT_CHAINS", + "MPNN_ALPHABET", +] diff --git a/examples/impress_r/mpnn_train_wrapper.py b/examples/impress_r/mpnn_train_wrapper.py new file mode 100644 index 0000000..01ae8d5 --- /dev/null +++ b/examples/impress_r/mpnn_train_wrapper.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Standalone ProteinMPNN fine-tuning wrapper — ROME-A's training executable. + +This is the script the training manager submits **as a shell command**, the same +way IMPRESS submits ``mpnn_wrapper.py`` for inference rather than calling a Python +function inside the campaign process. The manager stages the round's structures, +writes a self-contained job spec (a JSON file), and runs:: + + python examples/impress_r/mpnn_train_wrapper.py --job + +The round therefore executes in its *own* process on its own GPU: nothing about +the fine-tune lives in the manager's address space, and the process exits when +the round finishes, so the CUDA context and the model are released with it. + +It sits beside the inference wrapper (``mpnn_wrapper.py``) it complements, in the +IMPRESS-R example rather than in the framework — the trainer is an integration, +and ROME-A itself is workflow-agnostic. The file is deliberately dragon-free — it +imports only the standard library, torch, and the ``dauparas/ProteinMPNN`` +checkout named in the job — so it can be run and debugged on its own, exactly +like ``mpnn_wrapper.py``. It is also the single source of truth for the training +loop: ``mpnn.py`` imports :func:`run_round` for the in-process path, so there is +only one copy of the loop. + +Job spec (all keys written by ``ProteinMPNNTrainer``):: + + { + "mpnn_repo": "/path/to/ProteinMPNN", # the checkout, for its training modules + "resume_from": "/path/to/v_48_020.pt", # initial weights, or the previous round + "target_weights": "/path/to/out.pt", # where to write the new checkpoint + "designs": [ # one per staged structure + {"name": "d0", "path": "/stage/d0.pdb", + "designed_chains": ["A"], "context_chains": ["B"]} + ], + "hyperparams": { ... } # architecture + Noam schedule + } +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from typing import Any, Dict, Tuple + + +def _import_proteinmpnn(mpnn_repo: str): + """Import the checkout's *training* modules and ``parse_PDB``. + + The dauparas repo ships two ``ProteinMPNN`` classes: ``protein_mpnn_utils`` + (inference; ``forward`` takes ``randn``) and ``training/model_utils`` + (training; ``forward`` generates the decoding order itself). The fine-tune + must use the *training* one, together with the repo's ``featurize``, + ``loss_smoothed`` and ``NoamOpt``. ``parse_PDB`` comes from the inference + module. + + Returns ``(parse_PDB, featurize, loss_smoothed, NoamOpt, ProteinMPNN, + StructureDataset, StructureLoader)`` — the exact objects + ``training/training.py`` uses. + """ + training_dir = os.path.join(mpnn_repo, "training") + if not os.path.isdir(training_dir): + raise FileNotFoundError( + f"{mpnn_repo!r} has no training/ directory — is this a " + "dauparas/ProteinMPNN checkout? The trainer needs its " + "training/model_utils.py and training/utils.py." + ) + # training/ first so `model_utils`/`utils` resolve to the training copies, + # then the repo root for `protein_mpnn_utils`. + for path in (mpnn_repo, training_dir): + if path not in sys.path: + sys.path.insert(0, path) + + from model_utils import ( # type: ignore # training/model_utils.py + NoamOpt, + ProteinMPNN, + featurize, + loss_smoothed, + ) + from protein_mpnn_utils import parse_PDB # type: ignore # repo root + from utils import StructureDataset, StructureLoader # type: ignore # training/utils.py + + return (parse_PDB, featurize, loss_smoothed, NoamOpt, ProteinMPNN, + StructureDataset, StructureLoader) + + +def _pdb_dicts(job: Dict[str, Any], parse_PDB) -> list: + """Parse each staged structure and attach its chain mask. + + The designed chain(s) go in ``masked_list`` (predicted and scored); anything + else present is ``visible_list`` (context — its backbone and true sequence + condition the prediction but are excluded from the loss). ``featurize`` turns + that into ``chain_M``. This is the dimer split IMPRESS-R relies on: "design + chain A given chain B". + """ + out = [] + for design in job["designs"]: + designed = list(design.get("designed_chains") or []) + for entry in parse_PDB(design["path"]): + present = [k[len("seq_chain_"):] for k in entry + if k.startswith("seq_chain_")] + masked = [c for c in designed if c in present] + visible = [c for c in present if c not in masked] + entry["name"] = design["name"] + entry["masked_list"] = masked + entry["visible_list"] = visible + out.append(entry) + return out + + +def run_round(job: Dict[str, Any]) -> str: + """Run one fine-tuning round from a job spec; return the checkpoint path. + + Mirrors ``training/training.py``'s inner loop exactly, using the checkout's + own ``featurize`` / ``loss_smoothed`` / ``NoamOpt`` and the training + ``ProteinMPNN``. The loss is over ``mask * chain_M`` — resolved residues of + the designed chain(s) only. The checkpoint is written in the original + ``{"model_state_dict", "num_edges", ...}`` format that ``protein_mpnn_run.py`` + loads, from a CPU snapshot so the GPU copy can be freed straight after. + """ + import gc + + import torch + + hp = job["hyperparams"] + (parse_PDB, featurize, loss_smoothed, NoamOpt, ProteinMPNN, + StructureDataset, StructureLoader) = _import_proteinmpnn(job["mpnn_repo"]) + + torch.manual_seed(int(hp["seed"])) + device = torch.device(hp["device"] if torch.cuda.is_available() else "cpu") + hd = int(hp["hidden_dim"]) + nlayers = int(hp["num_layers"]) + nedges = int(hp["num_neighbors"]) + noise = float(hp["backbone_noise"]) + model = optimizer = None + try: + dataset = StructureDataset( + _pdb_dicts(job, parse_PDB), verbose=False, truncate=None, + max_length=int(hp["max_protein_length"]), + ) + if len(dataset) == 0: + raise RuntimeError( + "no structures survived parsing/length filtering " + f"(max_protein_length={hp['max_protein_length']}); nothing to train on." + ) + loader = StructureLoader(dataset, batch_size=int(hp["batch_tokens"])) + + # -- model at the public v_48 architecture, resume prior weights ----- + model = ProteinMPNN( + num_letters=21, node_features=hd, edge_features=hd, hidden_dim=hd, + num_encoder_layers=nlayers, num_decoder_layers=nlayers, + k_neighbors=nedges, augment_eps=noise, dropout=float(hp["dropout"]), + ).to(device) + + step = 0 + resume = job.get("resume_from") + if resume: + state = torch.load(resume, map_location=device) + model.load_state_dict(state["model_state_dict"] if "model_state_dict" + in state else state) + step = int(state.get("step", 0)) # continue the Noam schedule + model.train() + + optimizer = NoamOpt( + hd, float(hp["learning_rate_factor"]), int(hp["warmup_steps"]), + torch.optim.Adam(model.parameters(), lr=0.0, betas=(0.9, 0.98), + eps=1e-9), + step, + ) + + gradient_norm = hp.get("gradient_norm") + # -- the fine-tuning loop (training/training.py, verbatim) ----------- + for _epoch in range(int(hp["max_epochs"])): + for batch in loader: + X, S, mask, lengths, chain_M, residue_idx, mask_self, \ + chain_encoding_all = featurize(batch, device) + optimizer.zero_grad() + mask_for_loss = mask * chain_M + log_probs = model(X, S, mask, chain_M, residue_idx, + chain_encoding_all) + _, loss = loss_smoothed(S, log_probs, mask_for_loss, + weight=float(hp["label_smoothing"])) + loss.backward() + if gradient_norm and float(gradient_norm) > 0.0: + torch.nn.utils.clip_grad_norm_(model.parameters(), + float(gradient_norm)) + optimizer.step() + step += 1 + + # Snapshot to CPU before the finally frees the GPU copy. + cpu_state = {k: v.detach().cpu() for k, v in model.state_dict().items()} + ckpt = { + "model_state_dict": cpu_state, + "num_edges": nedges, # protein_mpnn_run.py reads this + "noise_level": noise, + "step": int(step), + "optimizer_state_dict": optimizer.optimizer.state_dict(), + } + target = job["target_weights"] + os.makedirs(os.path.dirname(os.path.abspath(target)) or ".", exist_ok=True) + # Write beside the target then replace, so a reader (IMPRESS mid-pass) + # never sees a half-written weights file. + tmp = target + ".tmp" + torch.save(ckpt, tmp) + os.replace(tmp, target) + + # Completion marker, written LAST, into this round's output_dir. The + # training manager polls for it to detect that the round finished, even + # when the execution backend never delivers the task's result (a Dragon + # defect — see docs/dragon.md). It has to be this marker rather than the + # checkpoint itself: with publish_into_repo the checkpoint is a stable + # path that already exists from the previous round. Name kept in sync + # with rome.trainer.TRAIN_COMPLETE_MARKER. + output_dir = job.get("output_dir") + if output_dir: + with open(os.path.join(output_dir, "train_complete"), "w") as fd: + fd.write(target) + return target + finally: + # Release the GPU as soon as the round ends — see the module docstring. + del model, optimizer + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + +def main(argv: Tuple[str, ...] = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--job", required=True, + help="path to the job spec JSON written by the trainer") + args = parser.parse_args(argv) + + with open(args.job) as fd: + job = json.load(fd) + checkpoint = run_round(job) + # The last stdout line is the checkpoint path, for a caller reading stdout. + print(checkpoint) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/impress_r/mpnn_wrapper.py b/examples/impress_r/mpnn_wrapper.py new file mode 100644 index 0000000..811fd4d --- /dev/null +++ b/examples/impress_r/mpnn_wrapper.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +import argparse +parser = argparse.ArgumentParser() +import subprocess +#-db DATABSE -u USERNAME -p PASSWORD -size 20 +parser.add_argument("-pdb", "--input_path", help="Input path", type=str) +parser.add_argument("-out", "--output_path", help="Output path", type=str) +parser.add_argument("-mpnn", "--mpnn_path", help="MPNN path", type=str) +parser.add_argument("-seqs", "--seqs", help="How many sequences designs would you like?", type=int) +parser.add_argument("-is_monomer", "--is_monomer", help="Is your input a monomer? (1 or 0)", type=int) +parser.add_argument("-chains", "--design_chains", help="Which input chains do you want designed? Separate with spaces. Example: -chains='A B'", type=str) +parser.add_argument("-index", "--index", help="Which specific indices do you want designed/fixed? Separate with spaces. For multiple chains, separate with comma. Example: -index='1 3 12, 2 7 14 56'", type=str) +parser.add_argument("-fix", "--fix", help="Would you like these positions fixed? (1 or 0)", type=int) +parser.add_argument("-tie", "--tie", help="Which specific indices across multiple chains do you want tied? Separate indices with spaces, chains with comma. Lists must be of same length. Example: -tie='1 2 3 4 5, 1 2 3 4 5'", type=str) +parser.add_argument("-homo", "--homo", help="Are your input files homomers? (0 or 1) Note: Overrides tied positions. If homomer is specified, all positions on designed chains will be tied. If positions are restricted, lists must be of same length. Example: -index='1 3 5 7, 1 3 5 7'", type=int) +parser.add_argument("-bias_AA", "--bias_AA", help="For which amino acids would you like to install bias? Example: -bias_AA='D E H'", type=str) +parser.add_argument("-bias_weight", "--bias_weight", help="What weights would you like to install for the biased amino acids? Lists must match in length. Example: -bias_weight='0.3 -0.3 0.5'", type=str) +parser.add_argument("-temp", "--temp", help="What temperature would you like to sample from? Example: 0.3", type=float, default=0.1) +parser.add_argument("-inter", "--interface", help="Would you like to design the interface? Do not specify indices if designing the interface. (1 or 0)", type=int, default=0) + + + +args = parser.parse_args() + +input_path=args.input_path +output_path=args.output_path +mpnn_path=args.mpnn_path +is_monomer=args.is_monomer #default is_monomer false +chains=args.design_chains +if chains is None: + chains='A' #default design chain A +index=args.index +fix=args.fix #default false, specify non fixed +seqs=args.seqs +if seqs is None: + seqs=1 #default 1 design per structure +tie=args.tie +homo=args.homo +interface=args.interface +temp=args.temp +bias_AA=args.bias_AA +bias_weight=args.bias_weight + +if bias_weight!=None and bias_AA!=None: + path_for_bias=output_path+"/bias_pdbs.jsonl" + subprocess.call(['python', mpnn_path+"/helper_scripts/make_bias_AA.py", '--output_path='+path_for_bias, '--AA_list='+bias_AA, '--bias_list='+bias_weight]) +else: + path_for_bias='' + + +path_for_parsed_chains=output_path+"/parsed_pdbs.jsonl" + +if is_monomer==True: #monomer - no need for interface or tie functionality + subprocess.call(['python', mpnn_path+"/helper_scripts/parse_multiple_chains.py", '--input_path='+input_path, '--output_path='+path_for_parsed_chains]) + + if index != None: #check if indices are specified + path_for_assigned_chains=output_path+"/assigned_pdbs.jsonl" + path_for_fixed_positions=output_path+"/fixed_pdbs.jsonl" + subprocess.call(['python', mpnn_path+"/helper_scripts/assign_fixed_chains.py", '--input_path='+path_for_parsed_chains, '--output_path='+path_for_assigned_chains, '--chain_list=A']) + + if fix==True: #check if indices are fixed or designed + subprocess.call(['python', mpnn_path+"/helper_scripts/make_fixed_positions_dict.py", '--input_path='+path_for_parsed_chains, '--output_path='+path_for_fixed_positions, '--chain_list=A', '--position_list='+index]) + else: + subprocess.call(['python', mpnn_path+"/helper_scripts/make_fixed_positions_dict.py", '--input_path='+path_for_parsed_chains, '--output_path='+path_for_fixed_positions, '--chain_list=A', '--position_list='+index, '--specify_non_fixed']) + + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--fixed_positions_jsonl='+path_for_fixed_positions, '--bias_AA_jsonl='+path_for_bias]) + #monomeric, fixed + else: + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--bias_AA_jsonl='+path_for_bias]) + #monomeric, unfixed + +else: #multimer + subprocess.call(['python', mpnn_path+"/helper_scripts/parse_multiple_chains.py", '--input_path='+input_path, '--output_path='+path_for_parsed_chains]) + path_for_assigned_chains=output_path+"/assigned_pdbs.jsonl" + subprocess.call(['python', mpnn_path+"/helper_scripts/assign_fixed_chains.py", '--input_path='+path_for_parsed_chains, '--output_path='+path_for_assigned_chains, '--chain_list='+chains]) + + if index != None: #check if indices are specified + + path_for_fixed_positions=output_path+"/fixed_pdbs.jsonl" + + if fix==True: #check if indices are fixed or designed + subprocess.call(['python', mpnn_path+"/helper_scripts/make_fixed_positions_dict.py", '--input_path='+path_for_parsed_chains, '--output_path='+path_for_fixed_positions, '--chain_list='+chains, '--position_list='+index]) + else: + subprocess.call(['python', mpnn_path+"/helper_scripts/make_fixed_positions_dict.py", '--input_path='+path_for_parsed_chains, '--output_path='+path_for_fixed_positions, '--chain_list='+chains, '--position_list='+index, '--specify_non_fixed']) + + if homo==True: #check for homomer + path_for_tied_positions=output_path+"/tied_pdbs.jsonl" + subprocess.call(['python', mpnn_path+"/helper_scripts/make_tied_positions_dict.py", '--input_path='+path_for_parsed_chains,'--output_path='+path_for_tied_positions, '--chain_list='+chains, '--homooligomer=1']) + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--fixed_positions_jsonl='+path_for_fixed_positions, '--tied_positions_jsonl='+path_for_tied_positions, '--bias_AA_jsonl='+path_for_bias]) + #multimeric, fixed, homomer + + elif tie!=None: #check for tied positions + + path_for_tied_positions=output_path+"/tied_pdbs.jsonl" + subprocess.call(['python', mpnn_path+"/helper_scripts/make_tied_positions_dict.py", '--input_path='+path_for_parsed_chains,'--output_path='+path_for_tied_positions, '--chain_list='+chains, '--position_list='+tie]) + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--fixed_positions_jsonl='+path_for_fixed_positions, '--tied_positions_jsonl='+path_for_tied_positions, '--bias_AA_jsonl='+path_for_bias]) + #multimeric, fixed, tied + + else: + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--fixed_positions_jsonl='+path_for_fixed_positions, '--bias_AA_jsonl='+path_for_bias]) + #multimeric, fixed, untied + + elif interface==True: + path_for_fixed_positions=output_path+"/interface_dict.jsonl" + + subprocess.call(['python', mpnn_path+"/helper_scripts/mk_interface_dict.py", '--input_path='+input_path, '--output_path='+path_for_fixed_positions]) + + if homo==True: #check for homomer + path_for_tied_positions=output_path+"/tied_pdbs.jsonl" + subprocess.call(['python', mpnn_path+"/helper_scripts/make_tied_positions_dict.py", '--input_path='+path_for_parsed_chains,'--output_path='+path_for_tied_positions, '--chain_list='+chains, '--homooligomer=1']) + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--fixed_positions_jsonl='+path_for_fixed_positions, '--tied_positions_jsonl='+path_for_tied_positions, '--bias_AA_jsonl='+path_for_bias]) + #multimeric, interface, homomer + + elif tie!=None: #check for tied positions + + path_for_tied_positions=output_path+"/tied_pdbs.jsonl" + subprocess.call(['python', mpnn_path+"/helper_scripts/make_tied_positions_dict.py", '--input_path='+path_for_parsed_chains,'--output_path='+path_for_tied_positions, '--chain_list='+chains, '--position_list='+tie]) + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--fixed_positions_jsonl='+path_for_fixed_positions, '--tied_positions_jsonl='+path_for_tied_positions, '--bias_AA_jsonl='+path_for_bias]) + #multimeric, interface, tied + + else: + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--fixed_positions_jsonl='+path_for_fixed_positions, '--bias_AA_jsonl='+path_for_bias]) + #multimeric, interface, untied + else: + + if homo==True: #check for homomer + path_for_tied_positions=output_path+"/tied_pdbs.jsonl" + subprocess.call(['python', mpnn_path+"/helper_scripts/make_tied_positions_dict.py", '--input_path='+path_for_parsed_chains,'--output_path='+path_for_tied_positions, '--chain_list='+chains, '--homooligomer=1']) + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--tied_positions_jsonl='+path_for_tied_positions, '--bias_AA_jsonl='+path_for_bias]) + #multimeric, unfixed, homomer + + elif tie!=None: #check for tied positions + + path_for_tied_positions=output_path+"/tied_pdbs.jsonl" + subprocess.call(['python', mpnn_path+"/helper_scripts/make_tied_positions_dict.py", '--input_path='+path_for_parsed_chains,'--output_path='+path_for_tied_positions, '--chain_list='+chains, '--position_list='+tie]) + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--tied_positions_jsonl='+path_for_tied_positions, '--bias_AA_jsonl='+path_for_bias]) + #multimeric, unfixed, tied + + else: + subprocess.call(['python', mpnn_path+"/protein_mpnn_run.py", '--jsonl_path='+path_for_parsed_chains,'--out_folder='+output_path, '--num_seq_per_target='+str(seqs), '--sampling_temp='+str(temp), '--seed=37', '--batch_size=1', '--chain_id_jsonl='+path_for_assigned_chains, '--bias_AA_jsonl='+path_for_bias]) + #multimeric, unfixed, untied \ No newline at end of file diff --git a/examples/impress_r/plddt_extract_pipeline.py b/examples/impress_r/plddt_extract_pipeline.py new file mode 100644 index 0000000..6dc57bd --- /dev/null +++ b/examples/impress_r/plddt_extract_pipeline.py @@ -0,0 +1,63 @@ +import numpy as np +import os +import pandas as pd +import json +import argparse + +# Peptide EGYQDYEPEA is 10 residues and always placed last in the Boltz FASTA +PEP_LEN = 10 + +parser = argparse.ArgumentParser() +parser.add_argument('--iter', type=str, help='pass iteration') +parser.add_argument('--out', type=str, help='pipeline name') +parser.add_argument('--path', type=str, help='base path') +args = parser.parse_args() + +af_path = os.path.join(args.path, 'af_pipeline_outputs_multi', args.out, 'af/prediction') +dimer_models_path = os.path.join(af_path, 'dimer_models') + +rows = [] + +for name in os.listdir(dimer_models_path): + pred_dir = os.path.join( + dimer_models_path, name, f"boltz_results_{name}", "predictions", name + ) + plddt_file = os.path.join(pred_dir, f"plddt_{name}_model_0.npz") + conf_file = os.path.join(pred_dir, f"confidence_{name}_model_0.json") + pae_file = os.path.join(pred_dir, f"pae_{name}_model_0.npz") + + if not all(os.path.exists(f) for f in [plddt_file, conf_file, pae_file]): + continue + + # avg_plddt: mean per-residue pLDDT (Boltz stores 0-1; scale to 0-100) + plddt = np.load(plddt_file)['plddt'] + avg_plddt = float(plddt.mean() * 100) + + # iptm from Boltz confidence JSON — interface PTM is the primary binding quality metric + with open(conf_file) as f: + conf = json.load(f) + iptm = conf.get('iptm', conf.get('ptm', 0.0)) + + # avg_pae: mean cross-chain PAE between PDZ domain and peptide + # Boltz outputs PDZ residues first, peptide last; PAE matrix is (N, N) in Angstroms + pae_matrix = np.load(pae_file)['pae'] + total_res = pae_matrix.shape[0] + pdz_len = total_res - PEP_LEN + cross_pae = np.concatenate([ + pae_matrix[:pdz_len, pdz_len:].ravel(), + pae_matrix[pdz_len:, :pdz_len].ravel(), + ]) + avg_pae = float(cross_pae.mean()) if len(cross_pae) > 0 else 0.0 + + rows.append({ + 'ID': f"{name}.pdb", + 'avg_plddt': avg_plddt, + 'ptm': iptm, + 'avg_pae': avg_pae, + }) + +print(f"Processed {len(rows)} structure(s)") + +df = pd.DataFrame(rows, columns=['ID', 'avg_plddt', 'ptm', 'avg_pae']) +csv_path = os.path.join(args.path, 'af_stats_' + args.out + '_pass_' + args.iter + '.csv') +df.to_csv(csv_path, index=False) diff --git a/examples/impress_r/plot_rome_scores.py b/examples/impress_r/plot_rome_scores.py new file mode 100644 index 0000000..9426ad2 --- /dev/null +++ b/examples/impress_r/plot_rome_scores.py @@ -0,0 +1,229 @@ +""" +IMPRESS-R ROME score analysis. + +Produces three plots: + 1. Per-pipeline avg_pLDDT trajectory over passes (depth-0 pipelines only, one line each) + 2. Box plot of avg_pLDDT distribution per pass across all pipelines and depths + 3. Global mean per pass (all depths) with linear regression trend + +Usage: + python plot_rome_scores.py [--csv-dir DIR] [--log FILE] [--out-dir DIR] + +Defaults: + --csv-dir /scratch/bblj/$USER/IMPRESS_outputs + --log logs/impress_21736435.out (relative to script dir) + --out-dir . (saves next to this script) +""" + +import argparse +import csv +import os +import re +import statistics +from collections import defaultdict + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker +import numpy as np + +# ── Argument parsing ────────────────────────────────────────────────────────── +parser = argparse.ArgumentParser() +_user = os.environ.get("USER", "mgoliyad1") +_scratch = os.environ.get("SCRATCH", f"/scratch/bblj/{_user}") +parser.add_argument("--csv-dir", default=f"{_scratch}/IMPRESS_outputs") +parser.add_argument("--log", default=os.path.join(os.path.dirname(__file__), "logs", "impress_21736435.out")) +parser.add_argument("--out-dir", default=os.path.dirname(__file__)) +args = parser.parse_args() + +os.makedirs(args.out_dir, exist_ok=True) + +# ── Load pLDDT data ─────────────────────────────────────────────────────────── +# records: list of (root, pipeline, depth, pass_num, mean_plddt, [values]) +records = [] +pattern = re.compile(r"af_stats_(p\d+(?:_sub\d+)*?)_pass_(\d+)\.csv") + +for fname in os.listdir(args.csv_dir): + m = pattern.match(fname) + if not m: + continue + pipeline, pass_num = m.group(1), int(m.group(2)) + depth = pipeline.count("_sub") + root = pipeline.split("_sub")[0] + path = os.path.join(args.csv_dir, fname) + with open(path) as f: + vals = [float(r["avg_plddt"]) for r in csv.DictReader(f) if r.get("avg_plddt")] + if vals: + records.append((root, pipeline, depth, pass_num, statistics.mean(vals), vals)) + +# ── Load ROME training events from log ─────────────────────────────────────── +# Extract unique (round, timestamp) from log lines like: +# 12:02:36.532 [INFO] [ROME-TRAINER] submitting training round 1 (3 designs ...) -> v1 +rome_rounds = {} # round_num -> HH:MM as float minutes-since-start +log_start = None + +if os.path.exists(args.log): + rome_pat = re.compile(r"(\d{2}):(\d{2}):\d{2}\.\d+.*ROME-TRAINER.*submitting training round (\d+)") + with open(args.log) as f: + for line in f: + clean = re.sub(r"\x1b\[[0-9;]*m", "", line) + m = rome_pat.search(clean) + if m: + h, mi, rnd = int(m.group(1)), int(m.group(2)), int(m.group(3)) + t = h * 60 + mi + if log_start is None: + log_start = t + if rnd not in rome_rounds: + rome_rounds[rnd] = t - log_start # minutes since job start + +# ── Palette ─────────────────────────────────────────────────────────────────── +PIPELINES = sorted({r[0] for r in records}) +cmap = matplotlib.colormaps["tab20"].resampled(len(PIPELINES)) +COLORS = {p: cmap(i) for i, p in enumerate(PIPELINES)} + +ACCENT = "#E05C3A" # warm orange-red for trend / mean +GRID_COL = "#E8E8E8" + +plt.rcParams.update({ + "font.family": "sans-serif", + "axes.spines.top": False, + "axes.spines.right": False, + "axes.grid": True, + "grid.color": GRID_COL, + "grid.linewidth": 0.7, +}) + +# ═══════════════════════════════════════════════════════════════════════════════ +# Plot 1 — Per-pipeline trajectory (depth-0 only) +# ═══════════════════════════════════════════════════════════════════════════════ +fig1, ax1 = plt.subplots(figsize=(11, 6)) + +depth0 = [(root, pass_num, mean) for root, pipeline, depth, pass_num, mean, _ in records if depth == 0] + +pipe_data = defaultdict(dict) # root -> {pass_num: mean} +for root, pass_num, mean in depth0: + pipe_data[root][pass_num] = mean + +for pipe in sorted(pipe_data): + xs = sorted(pipe_data[pipe]) + ys = [pipe_data[pipe][x] for x in xs] + ax1.plot(xs, ys, marker="o", markersize=4, linewidth=1.5, + color=COLORS[pipe], label=pipe, alpha=0.85) + +ax1.set_xlabel("Pass", fontsize=11) +ax1.set_ylabel("avg_pLDDT", fontsize=11) +ax1.set_title("Per-pipeline avg_pLDDT over passes (root pipelines only)", fontsize=13, pad=10) +ax1.set_ylim(40, 100) +ax1.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) +ax1.legend(ncol=4, fontsize=8, loc="lower right", framealpha=0.7) +ax1.axhline(75, color="#999999", linewidth=0.8, linestyle="--", label="threshold 75") + +fig1.tight_layout() +out1 = os.path.join(args.out_dir, "rome_pipeline_trajectories.png") +fig1.savefig(out1, dpi=150) +print(f"Saved: {out1}") + +# ═══════════════════════════════════════════════════════════════════════════════ +# Plot 2 — Box plot per pass (all depths) +# ═══════════════════════════════════════════════════════════════════════════════ +pass_vals = defaultdict(list) +for root, pipeline, depth, pass_num, mean, vals in records: + pass_vals[pass_num].extend(vals) + +all_passes = sorted(pass_vals) +box_data = [pass_vals[p] for p in all_passes] +counts = [len(pass_vals[p]) for p in all_passes] + +fig2, ax2 = plt.subplots(figsize=(11, 6)) +bp = ax2.boxplot(box_data, positions=all_passes, widths=0.6, + patch_artist=True, showfliers=True, + flierprops=dict(marker=".", markersize=3, alpha=0.4, color="#AAAAAA"), + medianprops=dict(color=ACCENT, linewidth=2), + boxprops=dict(facecolor="#D6E8F5", alpha=0.85)) + +# Annotate n per pass +for x, n in zip(all_passes, counts): + ax2.text(x, 38.5, f"n={n}", ha="center", fontsize=7, color="#666666") + +ax2.set_xlabel("Pass", fontsize=11) +ax2.set_ylabel("avg_pLDDT", fontsize=11) +ax2.set_title("avg_pLDDT distribution per pass (all pipelines and depths)", fontsize=13, pad=10) +ax2.set_ylim(36, 100) +ax2.axhline(75, color="#999999", linewidth=0.8, linestyle="--") +ax2.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + +fig2.tight_layout() +out2 = os.path.join(args.out_dir, "rome_pass_distribution.png") +fig2.savefig(out2, dpi=150) +print(f"Saved: {out2}") + +# ═══════════════════════════════════════════════════════════════════════════════ +# Plot 3 — Global mean per pass + regression + ROME round markers +# ═══════════════════════════════════════════════════════════════════════════════ +pass_means = {p: statistics.mean(v) for p, v in pass_vals.items()} +xs_all = sorted(pass_means) +ys_all = [pass_means[x] for x in xs_all] + +# Linear regression over the mean trajectory +coef = np.polyfit(xs_all, ys_all, 1) +trend_y = np.polyval(coef, xs_all) + +fig3, ax3 = plt.subplots(figsize=(11, 6)) + +ax3.plot(xs_all, ys_all, marker="o", markersize=6, linewidth=2, + color=ACCENT, label="global mean avg_pLDDT", zorder=3) +ax3.plot(xs_all, trend_y, linewidth=1.2, linestyle="--", + color="#888888", label=f"trend (slope {coef[0]:+.3f}/pass)", zorder=2) +ax3.fill_between(xs_all, + [statistics.mean(pass_vals[p]) - statistics.stdev(pass_vals[p]) if len(pass_vals[p]) > 1 else pass_means[p] for p in xs_all], + [statistics.mean(pass_vals[p]) + statistics.stdev(pass_vals[p]) if len(pass_vals[p]) > 1 else pass_means[p] for p in xs_all], + alpha=0.12, color=ACCENT, label="±1 stdev") + +# Add per-pass mean labels +for x, y in zip(xs_all, ys_all): + ax3.text(x, y + 0.6, f"{y:.1f}", ha="center", fontsize=8, color=ACCENT) + +ax3.set_xlabel("Pass", fontsize=11) +ax3.set_ylabel("Mean avg_pLDDT", fontsize=11) +ax3.set_title("Global mean avg_pLDDT per pass with ROME training events", fontsize=13, pad=10) +ax3.set_ylim(60, 100) +ax3.axhline(75, color="#BBBBBB", linewidth=0.8, linestyle=":") + +# ROME round markers on a secondary x-axis band (minutes since start) +if rome_rounds: + ax3b = ax3.twiny() + ax3b.set_xlim(ax3.get_xlim()) + # Map ROME round to pass by aligning their timing with pass indices + # Annotate as vertical bands on the main axis (approximated) + for rnd in sorted(rome_rounds)[:8]: # show first 8 to avoid clutter + # Place marker between pass 1 and max pass proportionally + t_frac = rome_rounds[rnd] / max(rome_rounds.values()) if max(rome_rounds.values()) > 0 else 0 + x_pos = xs_all[0] + t_frac * (xs_all[-1] - xs_all[0]) + ax3.axvline(x_pos, color="#99AACC", linewidth=0.6, linestyle=":", alpha=0.7) + ax3.text(x_pos, 61.5, f"v{rnd}", fontsize=6.5, color="#667799", + ha="center", rotation=90, va="bottom") + ax3b.set_visible(False) + +ax3.legend(fontsize=9, loc="lower left") + +fig3.tight_layout() +out3 = os.path.join(args.out_dir, "rome_global_trend.png") +fig3.savefig(out3, dpi=150) +print(f"Saved: {out3}") + +# ── Text summary ────────────────────────────────────────────────────────────── +print("\n=== Per-pass global mean ===") +for p in xs_all: + vals = pass_vals[p] + print(f" pass {p:2d} mean={statistics.mean(vals):.2f} " + f"median={statistics.median(vals):.2f} n={len(vals)}") + +slope = coef[0] +print(f"\nLinear trend slope: {slope:+.4f} pLDDT / pass") +if slope > 0.2: + print(" → Positive trend: ROME appears to be improving sequence quality.") +elif slope < -0.2: + print(" → Negative trend: scores declining — may reflect harder proteins in later passes.") +else: + print(" → Flat trend: no clear improvement signal yet (expected early in a run).") diff --git a/examples/impress_r/protein_binding_rome.py b/examples/impress_r/protein_binding_rome.py new file mode 100644 index 0000000..c788b4b --- /dev/null +++ b/examples/impress_r/protein_binding_rome.py @@ -0,0 +1,335 @@ + +import asyncio +import copy +import os +import shutil + +from impress.pipelines.impress_pipeline import ImpressBasePipeline + +MPNN_PATH = os.environ.get("MPNN_PATH", "") + +_BOLTZ_CHAIN_MAP = {'pdz': 'A', 'pep': 'B'} + +def _copy_pdb_rename_chains(src, dst, chain_map=_BOLTZ_CHAIN_MAP): + """Copy a PDB, replacing Boltz multi-char chain IDs with standard single-char IDs.""" + with open(src) as f_in, open(dst, 'w') as f_out: + for line in f_in: + if line.startswith(('ATOM', 'HETATM', 'TER')): + chain = line[21:24] + if chain in chain_map: + line = line[:21] + chain_map[chain] + line[24:] + f_out.write(line) + + +class ProteinBindingPipeline(ImpressBasePipeline): + def __init__(self, name, flow, configs=None, **kwargs): + if configs is None: + configs = {} + + self.is_child: bool = kwargs.get("is_child", False) + self.passes = kwargs.get("passes", 1) + self.start_pass: int = kwargs.get("start_pass", 1) + self.step_id = kwargs.get("step_id", 1) + self.seq_rank = kwargs.get("seq_rank", 0) + self.num_seqs = kwargs.get("num_seqs", 10) + self.sub_order = kwargs.get("sub_order", 0) + self.max_passes = kwargs.get("max_passes", 10) + self.mpnn_path = kwargs.get("mpnn_path") or MPNN_PATH + if not self.mpnn_path: + raise ValueError("mpnn_path must be supplied via kwarg or MPNN_PATH env var") + self.peptide_seq: str = kwargs.get("peptide_seq", "EGYQDYEPEA") + self.policy = kwargs.get("policy", None) + + self.current_scores = {} + self.iter_seqs = kwargs.get("iter_seqs", {}) + self.previous_scores = kwargs.get("previous_scores", {}) + + super().__init__(name, flow, **configs, **kwargs) + + self.fasta_list_2 = kwargs.get("fasta_list_2", []) + self.base_path = kwargs.get("base_path", os.getcwd()) + self.input_base_path = kwargs.get("input_base_path", self.base_path) + self.output_base_path = kwargs.get("output_base_path", self.base_path) + self.scripts_path = os.path.join(self.base_path, "scripts") + self.input_path = os.path.join(self.input_base_path, f"prod_in/{self.name}_in") + + self.output_path = os.path.join( + self.output_base_path, "af_pipeline_outputs_multi", self.name + ) + self.output_path_mpnn = os.path.join(self.output_path, "mpnn") + self.output_path_af = os.path.join( + self.output_path, "af/prediction/best_models" + ) + + for file_name in os.listdir(self.input_path): + self.fasta_list_2.append(file_name) + + def set_up_new_pipeline_dirs(self, new_pipeline_name): + base_output = os.path.join( + self.output_base_path, "af_pipeline_outputs_multi", new_pipeline_name + ) + input_dir = os.path.join(self.input_base_path, f"prod_in/{new_pipeline_name}_in") + + subdirs = [ + "af/fasta", + "af/prediction", + "af/prediction/best_models", + "af/prediction/best_ptm", + "af/prediction/dimer_models", + "af/prediction/logs", + "mpnn", + *[f"mpnn/job_{i}" for i in range(1, self.max_passes + 1)], + ] + + paths_to_create = [input_dir, base_output] + [ + os.path.join(base_output, subdir) for subdir in subdirs + ] + + for path in paths_to_create: + os.makedirs(path, exist_ok=True) + + def register_pipeline_tasks(self): + + @self.auto_register_task(local_task=True) + async def s1(): + self.step_id += 1 + mpnn_script = os.path.join(self.base_path, "mpnn_wrapper.py") + output_dir = os.path.join(self.output_path_mpnn, f"job_{self.passes}") + os.makedirs(output_dir, exist_ok=True) + + chain = "A" + input_path = self.input_path if self.passes == 1 else self.output_path_af + + cmd = ( + f"bash {self.scripts_path}/s1_mpnn.sh " + f"{mpnn_script} " + f"{input_path} " + f"{output_dir} " + f"{self.mpnn_path} " + f"{self.num_seqs} " + f"{chain}" + ) + log_path = os.path.join(output_dir, "mpnn_run.log") + with open(log_path, "w") as lf: + proc = await asyncio.create_subprocess_shell( + cmd, stdout=lf, stderr=asyncio.subprocess.STDOUT, + env=self._gpu_env(), + ) + rc = await proc.wait() + if rc != 0: + raise RuntimeError(f"s1 MPNN failed (exit {rc})") + + @self.auto_register_task(local_task=True) + async def s2(): + self.step_id += 1 + job_seqs_dir = f"{self.output_path_mpnn}/job_{self.passes}/seqs" + + for file_name in os.listdir(job_seqs_dir): + seqs = [] + with open(os.path.join(job_seqs_dir, file_name)) as fd: + lines = fd.readlines()[2:] + + score = None + for line in lines: + line = line.strip() + if line.startswith(">"): + score = float(line.split(",")[2].replace(" score=", "")) + else: + seqs.append([line, score]) + + seqs.sort(key=lambda x: x[1]) + self.iter_seqs[file_name.split(".")[0]] = seqs + + @self.auto_register_task(local_task=True) + async def s3(): + self.step_id += 1 + output_dir = os.path.join(self.output_path, "af", "fasta") + msa_cache = os.path.expanduser("~/boltz/msa_cache") + + fasta_file_to_return = [] + for fasta_file in self.fasta_list_2: + base_name = fasta_file.split(".")[0] + fasta_file_to_return.append(base_name) + design_seq = self.iter_seqs[base_name][self.seq_rank][0] + pep_seq = self.peptide_seq + + pdz_msa = os.path.join( + msa_cache, f"boltz_results_{base_name}", "msa", f"{base_name}_0.csv" + ) + pep_msa = os.path.join( + msa_cache, f"boltz_results_{base_name}", "msa", f"{base_name}_1.csv" + ) + if os.path.exists(pdz_msa) and os.path.exists(pep_msa): + pdz_tag = f">pdz|protein|{pdz_msa}" + pep_tag = f">pep|protein|{pep_msa}" + else: + pdz_tag = ">pdz|protein|empty" + pep_tag = ">pep|protein|empty" + + fasta_path = os.path.join(output_dir, f"{base_name}.fa") + with open(fasta_path, "w") as f: + f.write(f"{pdz_tag}\n{design_seq}\n{pep_tag}\n{pep_seq}\n") + + return fasta_file_to_return + + @self.auto_register_task(local_task=True) + async def s4(target_fasta): + self.step_id += 1 + cmd = ( + f"bash {self.scripts_path}/s4_boltz.sh " + f"{self.output_path}/af/fasta/{target_fasta}.fa " + f"{self.output_path}/af/prediction/dimer_models/{target_fasta}" + ) + self.logger.pipeline_log(f"s4 command for {target_fasta}: {cmd}") + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + env=self._gpu_env(), + ) + rc = await proc.wait() + if rc != 0: + raise RuntimeError(f"Boltz failed for {target_fasta} (exit {rc})") + + @self.auto_register_task(local_task=True) + async def s4_post_exec( + target_fasta, + models_path, + best_model_pdb, + best_ptm_json, + mpnn_pdb + ): + self.step_id += 1 + _copy_pdb_rename_chains(f"{models_path}/{target_fasta}_model_0.pdb", best_model_pdb) + shutil.copy(f"{models_path}/confidence_{target_fasta}_model_0.json", best_ptm_json) + _copy_pdb_rename_chains(f"{models_path}/{target_fasta}_model_0.pdb", mpnn_pdb) + + @self.auto_register_task() + async def s5(): + self.step_id += 1 + return ( + f"bash {self.scripts_path}/s5_plddt_extract.sh " + f"{self.output_base_path} " + f"{self.passes} " + f"{self.name}" + ) + + async def get_scores_map(self): + return {"c_scores": self.current_scores, "p_scores": self.previous_scores} + + def finalize(self, sub_iter_seqs): + from pathlib import Path + for a in sub_iter_seqs: + self.fasta_list_2.remove(f"{a}.pdb") + Path(f"{self.output_path_af}/{a}.pdb").unlink(missing_ok=True) + Path(f"{self.output_path}/af/fasta/{a}.fa").unlink(missing_ok=True) + self.previous_scores = copy.deepcopy(self.current_scores) + + async def run(self): + self.logger.pipeline_log(f"Running for a maximum of {self.max_passes} passes") + + self.set_up_new_pipeline_dirs(self.name) + + while self.passes <= self.max_passes: + self.logger.pipeline_log(f"Starting pass {self.passes}") + + if self.is_child and self.passes == self.start_pass: + self.logger.pipeline_log( + "Skipping MPNN and Ranking steps for this child pipeline " + "in the current pass only." + ) + else: + self.logger.pipeline_log("Submitting MPNN task") + await self.s1() + self.logger.pipeline_log("MPNN task finished") + + self.logger.pipeline_log("Submitting sequence ranking task") + await self.s2() + self.logger.pipeline_log("Sequence ranking task finished") + + self.logger.pipeline_log("Submitting scoring task") + fasta_files = await self.s3() + self.logger.pipeline_log("Scoring task finished") + + alphafold_tasks = [] + post_exec_tasks = [] + + _boltz_sem = asyncio.Semaphore(2) + + async def _guarded_s4(target_fasta): + async with _boltz_sem: + return await self.s4(target_fasta=target_fasta) + + for target_fasta in fasta_files: + models_path = os.path.join( + self.output_path, "af", "prediction", "dimer_models", target_fasta, + f"boltz_results_{target_fasta}", "predictions", target_fasta + ) + best_model_pdb = os.path.join( + self.output_path, "af", "prediction", "best_models", + f"{target_fasta}.pdb", + ) + best_ptm_json = os.path.join( + self.output_path, "af", "prediction", "best_ptm", + f"{target_fasta}.json", + ) + mpnn_pdb = os.path.join( + self.output_path, "mpnn", f"job_{self.passes}", + f"{target_fasta}.pdb", + ) + + alphafold_tasks.append(_guarded_s4(target_fasta)) + post_exec_tasks.append( + self.s4_post_exec( + target_fasta=target_fasta, + models_path=models_path, + best_model_pdb=best_model_pdb, + best_ptm_json=best_ptm_json, + mpnn_pdb=mpnn_pdb, + ) + ) + + self.logger.pipeline_log( + f"Submitting {len(alphafold_tasks)} Boltz tasks asynchronously" + ) + s4_results = await asyncio.gather(*alphafold_tasks, return_exceptions=True) + self.logger.pipeline_log(f"{len(alphafold_tasks)} Boltz tasks finished") + for fasta_name, result in zip(fasta_files, s4_results): + if isinstance(result, Exception): + self.logger.pipeline_log(f"s4 FAILED for {fasta_name}: {result}") + else: + self.logger.pipeline_log(f"s4 DONE for {fasta_name}") + + s4_post_results = await asyncio.gather(*post_exec_tasks, return_exceptions=True) + any_s4_ok = False + for fasta_name, result in zip(fasta_files, s4_post_results): + if isinstance(result, Exception): + self.logger.pipeline_log(f"s4_post_exec FAILED for {fasta_name}: {result}") + else: + self.logger.pipeline_log(f"s4_post_exec DONE for {fasta_name}") + any_s4_ok = True + + if not any_s4_ok: + raise RuntimeError("All s4 tasks failed — skipping pLDDT extraction") + + self.logger.pipeline_log("Submitting pLDDT extraction task") + + staged_file = f"af_stats_{self.name}_pass_{self.passes}.csv" + await self.s5( + task_description={ + "output_staging": [ + { + "source": f"task:///{staged_file}", + "target": f"client:///{staged_file}", + } + ], + } + ) + self.logger.pipeline_log("pLDDT extraction finished") + + await self.run_adaptive_step(wait=True) + + if self.kill_parent: + break + + self.passes += 1 diff --git a/examples/impress_r/rome_global_trend.png b/examples/impress_r/rome_global_trend.png new file mode 100644 index 0000000000000000000000000000000000000000..0f1ff0c4c5d9d81f494739e5bbe0bcd1882291b2 GIT binary patch literal 107484 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
    1hSJWFMIFOh1chXE3vnk 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 Q7sA8Xqp~~Yx5H=uFN^p|UjP6A literal 0 HcmV?d00001 diff --git a/examples/impress_r/rome_pass_distribution.png b/examples/impress_r/rome_pass_distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..dd1e6fcc55fb7fcdb82642d0fea55c9c118baa7f GIT binary patch literal 46170 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 P+&|^8;-M7Tlb8PwOL|$a literal 0 HcmV?d00001 diff --git a/examples/impress_r/rome_pipeline_trajectories.png b/examples/impress_r/rome_pipeline_trajectories.png new file mode 100644 index 0000000000000000000000000000000000000000..a073b05d0eb228130813760c27c0f2698461345c GIT binary patch literal 131380 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 z%0D^a{lVj(U0*sRb?0Zk(9THHsOKD)Fe$si(OgN(%#Yg{#MqB|tbLPQD=m`W2&BKf z)BkOjNZ5}2Uw%bl7(g065!b2${B%{jIKf8FTLv+X7BJnf=K8kV5&1^>3R3qnz^@D- zCm~(0aONL3@ZSC zU9EaUX?vB_v;wDJ*`;4)TG9NqdMr0WiK%#Z?^cH3+um1Suh4|hoKC@Vc;qlOSlpsr zNKD*BeU?(#0oxBZ#DCmU@){L^K+;+lFOzSS8TKPIilph5om6W5nY$x4*TkpwhS0`S zi?+lqEVvk6Z#h+UKrwG28#hU0e~J)OhomN@z$eErxY>s-qkYX!=n^rp-wc0IoL|@E z-J(-6Hr`K-l-wUePG}Cn!|LwQcRLzg(|RBX#yCb#ZDn$qs{g_X>HWwm{KtD^aDIQx z!T6$eMt)BgWDF+taS!5|5tMtj6bjN!^Qt{Tqn6}A{3+jVyg$QnTJ9e-@!v#Ezf4Se ziE=F(ng9Gi*zt}ZucKL+p5&hbBsHE)1?IvfC{_O+)yfZe!F1E%QZ}f1PI&%cXF>7K z!zkb+SXjN+AK_WOg)gqz)wEG~FqVmuP8hM&2Y&L{xx6?OVZ)qzp$>I*Gm^_zHgtSA zye}Gfvi~Q|15sQdW;MPv0fJK=_djhK4*CBfJXkqI)^Tkhh)5cuRU*xVvpivp(f6zU1mdbm&n2b0lT(5eR?38u z$|ZFrROByP(N3{Xe{Uefl79sW>IAWZyM7I~A3C*8kT5nQs4OlH)-Y?^<9qjW}(|$vXY_OQm z?eh`u&6-XkhI46B_a~2FN0!qO+Y;f4FZX>?nb2X=8dKPG^xEw{HPnA#> z7EjKjsw)gsm&eRp<^~aX=Okb`ji}=2d?m&8DOc*YoHbr=S(pfr)*R`!LX-!;_?*S9 zi;&SmGgXJ0drhkyJ{<7wZKq@R6bd{eaTm}cwz$de%iriQbmW@}MQ+mCL3;(GNkbMQ zEZ;UriQT1MgPP%z1G$A6F{IN@sQ3DXpFp(ws);84=u8|Q{N>r3rm6mjQ8UoU4 zQ@G^ePEd$_`;BM*c1A2w89zR5nH>)u3WRZbUko&jo|5W|V5p0BT{>dtpMlX`RkHu< z2A-_jo6W>6i@W#AF9z5WdIOa=$zIQ_>1m^S2m@e6cweyb3Z6^vHp=MkHk!YB6Cm#y zOH?=*vXxv=29q3+IT-=3j2p58xB4CoVSxzkDR8N(#D zz92{wihg=?9pjer)2dE1ybvUqav)USsk0buW@8yO8-WVH^-{l6OC9WJccw0Rnh6KE zw1RtzaI|An=*{I1zT~!QJB=T%cU~}Ycp2|6TBQrgqRxH56v$}Hb+i3$Y)6CP(wwH~ zB1CR2=KWjJsm-k|<3?XX6B83TZEfv*e&Qdw9R5iXot)YA6l9|)e_NKQu1TWarwEwU z8L?nR6d>wAVX~U#rf<3{Be`e+iLm9SztQa|PzOGo*18_n^bD;6Rya`-2QGY?RJwjR~)ZerNgr99xV-x-F!;87@Ey;Jey7(&WuQynr0n-#Xo$A-rV2l z%1TR{P1g;hQ%@}^VH54q4d6{xx$n^TU^d3q-=HIkTy%20>+#MMhd?=nXXIw%HTN)ON~6QCC>{yg#rc-LgI%Dw!GGdqv=<0|jQ0{zGwT^;CczEw>fp>TMT^ zJjqY4Tyu|)V*U3$I7wFKMMmYFS3z*^h+-AbQp$;9+d)azdZuM~NNZ|pa#?+?lbv8l zTj!N*g7@5{eLQPpIBjQ&%F^m3!;jzjW~D-_oExLG!$NWto4U@swYh1uJY1}+MH#)Y zV^?q(`xbIid4F?A2>zO6T90J{@A|0RO-y;zX*A-15@doRT%-O?Y>XtG$;HgOUkAd< zwA~|<411Vq&SP0|)HC*}fMRG!6I-nB=TU1tMHO1SsphEhPF(|_FQ*1{k@~cE4}(gk8EYc)#_6iZX&kCI8Eqpn?0>GQ4Iywj)(_>xVX3rOG`Y7 zrV*(bP;d>_80xcQd^1|*Y#T{S9p6zoIXn~}I5{O4W7pZ~!;!?<_K|W}cV^hpFalz4oq#iv}xj{-igc4=&&YUCKUmFH0Z&lq4VykVO@Ax zG*i^t)=5r{QW{*QS)LpqxcTD!UC1>c-Nl-;IL4ynLhys+pfPaXe-@{qk=gs%B%vVn zkC?%vAo_xn6)KBCTd;%75~)==&zy0FHwr z#xzShA+?6wPf3k5c?-1F*#XCz2FLu(9B=BTNrx83^?L)OsdvKf7~B0l{PBUQ_nl(U zTa5m=XO4xH0k3hoA+}W!z@(_A(Sq|Q0y+hQAg*6Q-|6B}tKj=a4$InoUV!rB_cKwCi zGaK#w+VGVaxwaWIP!3obW4a-jgz>JuU?(_LN+ir}2^8+fvDhwiy{gBLx``1dmakOc zm#R{rKjbeFey5h5?0XDf>5CicSXlbAY`Dc8cv*mvkTbUWJGkLA79jPgW2wv=ujiS# zK43XB5|gRoLYW^q=-!mGZS6vf>6`vg7tg7?+Kq=1$M!z>AgV115!3k^*%Q; z3>F2+3t|aX@2BZ(OR`^Xef}&X#>#rixjH-TELKDeWQS7m%85|2GG8X7_T5~#C3cb> zegH11pKG%K0%H9eji=SokB6gvs6gi7-Ma6>UQ9q5Nk5KttXfAP#R*_Ze>36$*;2f^ zZM3n1lx0NaxJ*qa{nHKDe->K_{iXAb1hRVIL=fM;NgCMMjX9m>llKdSP4Bl`J-$+E z;QRBOfCR%O=Fy`TO!wfbqS(68TLSyL z5MaP^v*S8fbfY|>-3=~FfnU4HNKSA-aJrYJ*y`J-q!Mobq|$L0@iS-65D^l}Dk_E= zv_~X{g@ru}2gj0e0H&oSFJm8ioLh>}@MD{y^yeGQFLziX;^=Ueer8JScO1ymeQg}I@+0cI=TQw=# zvPPBJpqBxOGhWa-#0~UTp*5V>L_BF<1 zv6D!ZvRq(Rl1W$6DH}XYP|-tAP4Y{9lmIfImvN5XqfW=#J>Z%H+)YOWI}%I z~pfz$;d7#yiX*IxL^V6arm${OPWPc5lvh#N)PW8Pc zOl~yDi;zsEIp}|m)m}|)%?j*7RUN}r3|j2U)cy}2euewjd#ju`HtWvdQxW5lZTx)l z?)v;J|4}&?7gv$>q);{V4RI}I7mCT!0DVmUd?GkOSqU~>1YHo8)EWDrBfY~&)KrAj z*2HcW#q2Xs`Nmg|wL3(!zS)r+RC&k~LnmBKa&V6WL7fva^6p@57|MNKT-#;j&)I~A zcQ+P?P4oJBcSe)6k9G&48ie&VWOAd-uCWSx3nz6;XjLT9Re$w)5pd7qj$9}<%g@jR z%pkUw^%MhBf#M{z9IG^#+P!6xnCx9wJHOr zLUr{R-@P2u=Z;Fr-ZcQV%*@R6-}C%pYGQ&Ct*@^?8AA+2|BdLE$K{*c2%(-Zlrc)* zcRoPWV;FFB`9BIzE}}(y1E|DrgscuIu%sAN3v;h8T;pL z$c}v0oz|Hrf?K9Ub3oD6p~=Fju-`_5n$7)w>BV7b;MZ)6ddvvaAz&@}6YEn(4TtXZ=_hCGU5sHhk}`tPBPsA0 zW$5UXbLRkel=p60mfMTLW>s+60!GP6JJB&pCYoi)NDex=1F>q!iKGksBmnvaOr20^ z&%5)D&v)IDZnn(KrvZCq`bxjR#dCc5uShH*4G=arH(y;K+S=Np%m736nujXZKPmyF zwp6k9KwF48Z3xlEA|=aI#;OavIvJK!pkqfchGgKC_sdMn>xL?S3@)2Fsee1of{pg| z_Z#=7N;Vua!cc)zJ5g$!wQ_lmO~u9Ri7$EC{F7toS^^Qlx`mCnoJ4TO$bXUv2YdsV zGpIbueE5rF$8(giAitFZ3+#my-%E6xmzmZldwlQh%+?b(1won7)KA+_Q42D=X*R;h zW8R69JY5R!3H^8ew`dL3;1zehZq zP(TnybEi>yLT|khbk`($-X^5btXwi2G%UM;u%{wg?32C6vy5mLBb~Bya9GH#TscnD zK7Ra|wA~fSENnWE-QB@Hkxj^K5K;ll?q&Q`$WIzNj8d4w%tnIR0wuzym{21Q|3M9r zYZ42JPP}j))O4KpW$yfp@A2cMwIH$RaItB44W`4=qydjIJzcU`Ma(4Op{Cp#>HD0Mv_uYg2S^n^@*uy?Z(e9j`251f z*x2-8@l$;08$(|5k40a^yfhtigD4@#LF^$sdta+cWe^^18L@E;y~vlO$l-?AEJmqB zD>i$?0~V`ZHZiLK=$^b{-u|D!`Q&>#avdO2NstAcoXT#d{2ofeILVJN4TTlC-ye!u$erheIUQ|jAEh89YnuL;lp$Y8n4+*p z-1X#TMRTwD_Xm+0H$D#GzUb;+S^EM@s-vX^RMl7GdU38~UUV6TKHqLcqGPinLjZU*-Gkw{z29t{P z_~YW?nSfk|Y;^zVsD`iHuTkn)kk~T0mY$Qdtf`q)Q*($?-00+muFi^$PVR~AvUl_q zM(v&>B{{V}-_?FT!tfsn`%hjgajn*JhxXV(SRK0_)88x5E8qPxrlNw&e7Gp%3)xI^ z<&RI7rg!$5#!7XY@Wvb`+|@qlJV!Jko%WjAgOS>Pc#p1^Wve^DL*dd5`aZTp?wR(fSn z1c}T6ovj(QD7Apr)dzc|(3hOjYaHu&h|v`w7Oj{LQ)hAAHd`Jkm6ql|u)9cmjL*nb zvz4bcE3YQgesLh9xVZSU zrfr!K3ymnC9qG>g%MpB1$NLayCoLxOb$w>K;`1aZHag$(otv^Le^7g-q0aVLKgnP7 zO}ysRiRdR(Y;g}lPInQ$^)p1|6*j)SLG>cjaPnr%Kxs{iE7ZWfpUyGIV|*=YFa~t0 z1&`^&#fQw47;1jLuh}lnn{T0v@6+CI&^Bs#OTe{Swb3fOaPtT@_KxW8k|95BFH69l zTa&ev7)hAEv60bSvW6)au$gK4Q)yUn%-3#p-9!dX`LFZJ&yR|Vdh4)UaF!G7vlKON zs=XI=IC!+Ht-I~aLs~t{yV^Y&z@N)%Ixq+Q*aKj(n%lPx_4FR|LmRfe%vp)P+{ z34Q$(6!n!izLlO%PEPICO$E*yGuEZ0#l@iQ_4oBvz08!xE!U6CRdu&HcY7n!{F zUnlhM3#H^jex&X%lr&uJS}M`YGtqUYPW;&d#_-JcTI~+x)QT9W#|Jh9;*^#1Dn$nSuiA!()yV^x{ z?)Rc)+i9d6dI4a|)zFe7NzRvnfq@6JA-#?@8ziDVWUvX+PVE3Yw4yGX-NFIGhN@wt zu1gO@d*CRR<}2)97FcH|zkZwY&kIH$Uz~gU`0j9}$>fsEnkfq-!aXMi458 zKRkU2GORr~kd)JC6szgf24*vQVqXZKck^?EyW^OBxWnprPHHNJrq#!D4fMz50K{h` z&XD8VpyuCc@4tr@1|&BeDQV)Rxc+WF?`Stq|E71%kJk(#WuNl#946g%d*k^GvcC8d zGH-_FxjjK|<_EY-AY%66Lf)ZRxQ5LeB#evp#5#_1Qgitt*GFD;Q_AbCHfAFOtb-a! zaQaJ&f44iJ$heon9n+?*_u{|sTe#&VaXq76)J$cK z|A=N(PdGq|Mc}5x_4_Ib=Pq53*?)n|1$os`Crv$)l9vy*Dc8v0zkl*3yd5%0-vf^yCmnLbB+P%4@$aQ%XQ1r&sDK-=|;u_L3S*$j}= zSInnIU$Fc2rMe>Jbr#g07v?G!bLAZ!OXJ+PRYfsg2Edmy^YWA}E%QpuhTebs_EfZo zKv^RZdil2&2eb0>q~WlpXNKP`_JtqZ8c)4Q;+;7Id(B!p7ga7d9=A@-y@oUycR=TKUgw+Z4Q}4EXsIerlW!O zq<0fmOy>%fbKuoCk3&RI02}@be{;%FT|=X`wzsdBbi{DVqv3lv~@)R@zx-@@uD z*-n{cG}}DG(K-XU=nZW*>!*9BdDRr6Y87_IK__AzG9DF!BPZV+;7JCAUMbVT+hiq*vfWtz0E!~IIMWnl(}Q#K?~hchB% zp;>yr)(Pfb<>I(};!*4og@N``&Z>Lqft zYp6tX0U!U@w;A$Ot zb|@$cBOzgUWTY6@!+j%qq7Mk5xaMz@s7qE@zumODv#5PIt=%^^mV+p?h*=Wg`E0e) z=FP#`s-A4sLZoiFP_wtv+uSUY(e|6bN3g(_6YE8#BFUaRq?Lm;?t69_JLBHhDdV@^Yh)vSx%$=OhwS6 zyLW1j+(9?CpQgY3>qn45JPEWdg9zuo_D=g)RopED|vfH=QN=udb_rKxWBUA2I)}}MFG&b%=AVJgfU<)~t z)P@MeC$ERjA>_@#nOwjcj_)zjFfPb^1f48Hl?;DqO>9uxvzor_>*;chV2kw=MF)QP zum!e`keIk{X{az%1cee!HC})ObpQxQ9&+Xo`?Wv{J<`~C{L+yFkO=NDfN$EtVlD12 z<_%KvtzCqwT2kM?8IKD@I*{u6hK4e>x3}dyjecFgi#lcM-RHBXPYL`he{xKi`d6U9 z8jnN=C~=P>Iy6K)X?WWI+wIi};HS(h){>I&BqwLvH(9@Tm?{rgA9QVc-?I2zy)(Y& z@9$XI7WL0j0T?voiK_s^gU1N^w2QI+8d`H z{!&RjKT>*~Ml$}BS3)+pnj6B>QQ+>Hh*kHGkJF1XNy?|iIz%Bw0tgjH-7n5v+U(Es zQ0z7;B1W|GlUImh%gY!*b-)-=^hsI*?RWc}H*prSv=1^1hO%n_v%?D>|N4QKXd=X9 z9@6khj?CFD34#M@cY{Q}snj!~MByl83jM!sX$gLE(>oE^09ANf1~?&l%AvA@1YI~3Njt*;GR?27fMoKz)cQg<{2V+Hk=k?x}SU96o>PeiN zSj(Zuke^>>&71&+n+w~t;RKAn_9=$lHoVIhFCBlhKf48Cs%A6=3QBfLJh?U8D>CcT z%6bixHU+Bsyrh0Shh?W}J7qSOOWSaK+BE;cht_7r71(DE0_gfge)4RYYZ@Ysnff{g zHKJrO!ACxjD?dTv*lZnR6Mm8#9v`s4O2CnejfHM;k;l1jyK|!@guI*X6{e~wq25om zW+w+y9=HS&JWD#BFl9 zwc2|}2mB!(8+4o{nk)YVVwpdJP5i*mQtz2Msl36x6{=FJ)q;CV4;P=7or8)pJo+b` zqzLYy$@i$PINFq+#`c-k9#4Jf>T{Z8dA&29p`JzD@gJ+9x`YTGQcO(+A>$>IZ^8#? z;YU4|Rd-0r2mDNns0Xa|B2sbUrTtO*g*F94`)l5^9G(76p(>S9Z;BV!)6TzKFQp5v zt=4?L?SQ#XhyFG*!gXv|b$0c%d>~|>h5P@z(GT;(p#G1c?zz9uS8A-67E>P#n-+Yx zE?--0&KM z>izmfx*56?ph!525Jrwu{`%C__E}dJH=c^Q>}@AiTHxsp4A7Lh6u&UcRnlp|E*H47 zHSUsi#dMIm)U($2;Y0rsI@<8yV2M^u^{lU)hFQ~{-1-5ikl_N=j9hwhc5}I1KXbIc zUk+ucoXw;VIZ&fORQ+P8pza?^xg%;5z0RO@*HhTz zS~QGPe)@UvKKluRcm?|bFZa605th=om`7XgVV4taJ`&(urK%jhTA>=dTJfMhQJ0Ji z54rWwg$22(Ha=qwh9!H!`q*hE zTH1iDf6|l~H`pF8`6yjgF>`0S+JTPUt=6S4*6ovHTU*#Mk*gIK#X~dDL(A~$I%=s~TDwdg&|7}i^ z;Nf)JbZ!RqLC47*$9zZZGA(BGYr>U9b}HnqYxWD4f`Z=QJ49m_(JAX}(Y)^Dk&olF z^{-q^9mOeCKTHvy)~-;fm?7c}OKFd`q$xKpL{{3toXD{{zCBm7b!j+dj=_UkbiF+) zR9o8+V{?vccse(aVJqjKL2Zyhd*fi%#rp5AF!(u)Rc#GC2ykR)o8h+T)X1p=7DRv( zmfq9DC?Q|-9`8~H?)^Lj^gS|)97;3P+@gCi|)2!%lk^2!Q5Ts%^HtrEw)f@)U+SBT&k ztoDf-jpy5V=Z3@E#(HeT4tP%@@3k{vNjhp)1w+vMzq=(?^%D(KC)sdvl<)nL8~rfh zaj2As_tR!>)3B;SgN%iOHYmH;xbFBnEZWfw4W(#!9?8y5@9q(J?h-b{dkCTR2KuQ# zxRviOV{%ElvQ^KX^l4xrv|eGW3lEX=zV$h!n8cY8%Wy)x@Jb0R%E;grs&Z#rzB1KXts%`y z@OM`s+nPm#tw`*c@jC!n@5R#irR9@&ByXgDe{l&VTK@47?0}GPzCBH)YnSY-N_{<% z^9rXQ&rS@gMN#(7(K_2Xp~0cGlc?cBcC!1yB#-b?p(xPj*hC_htr@QEJ2D$wrPWdp z0)Y(T0`k5|`G(ZEIljc1A@{uyC*$fXi`sJQp~}W$q=9t^X#{Ls&gip}d z%4D+tvIJ*FXQvyb6zXnau2lK+Q`C_mJftm?vKCe{_ZYGj_e*OPe;%wFn&C4xa76l| zquKnt*%w2{Z0!u2zQG1z$Caxg_G)RFwrGI18tkLmui2r2|HRPQDiRrz(b$Vi$&OXj zL)#IyOOFNKgloxtPYFkfdQUJM73gDR&Dut$wc#%a4XUrZo&@V!8yZI|NQs4cmulH219x@ZK$ZF(tE zCcaRp4n)|-D2TQA-3lEola;HPY%R5Wb9$Ks+pegs)GkAVPrGIHL{~}(fiBi*mHNw^ z^L;!KsbrjB-(wBDy2U9PQLC~MEiN19+uRG?#`b<12Dh9KonL^NYaETMu@aVz$=v)G2cUfJOp6=9=2~? zo?y85XW*nmTkG@`DwruNi$KNN`-NdFc|GHlc1gBX?j%P69`Q|F`*GxMQXE$l90pee zO_zv2il&5kOyZ_YIO(=K6lN}bSc!GUOw6`FO|*|GfaepGq$$7IVxR`v36>!2C_~)% zV~>Di((asce8$(vEF|bWg)S$y+B9;#H0>Dxk*gF+wofac&66rhT&^FeW zT=$l_&uwp+2`@L)_V4nQ3NB!#*EEVVKaAzy*LQVog2(d5a)*wMusa;Y)&oY0)$C@X z^J-jvo)0t>vVFQtvI^0yGnN+M9;cUQ-t)%8@}Z0nbe_ZYPtl{}HM*p$k<6}k8p9@V z1A3qBUtZNK8(U}4q&ezI>0upghC#;NWFIFfp?JXmTIA2>M?0I-e6eIQFp=;&- zavOn>Wq4&xO<(CrD72D5qC&fs3LS`75V!A2^JM6G)}BUkqMvs$BO^_?*8UBQlFo)5 zXvcy3^X{v(W~K5O`zj@+4m5=uIo%VEtE? z;M6`mJef;C1^v?@%PUIH4*nn?s0~@Ih)jr+o2Kc0?Qi5!={Lq7ZAcwoC+q{+A zcHrqL>BI2K{O30=ujLNCd4*Vf`l3P1jUxQnSbpt~idolqY>O7X124<_!a)-VX}vw~ zTgBE@DQw{RPFe(^FK*NFUP6|{1p_c_qXmEn{ih{-l!rR zDO=`8`!OL~si?T3bScC;Ekiag^^$`1$8+dpAm?Z(tS5cp3LUGSO}EYd_7tH#LLdao zdFZ8jI;%gu7VUWGMwqJFvv<6VSiP4X;mJ@K=%+w3f^BDatprRRIAx-}qr-}V3xIf~ zD+->%GqfCZIi7z^5@S1 z9yH4yepE~{9g8Um$-Nb0Giq<2$+60#7~>e1yuWJ%1muz?cVy`}*8(y3b@}hFi^Zxw zD50mY|CqKk4X#bnd=B}CdeL890(r_X_74aA+Ts&^Zz&R?KfNM79lLKY2Y-*)? zWNan&DQSb*PTiV6<40h_5B5Txj1Ln1QmMPzl`u1%p@vc(fM*ao*;yZ88xcXN06J|#hr zS3khl*Yi%BoWr1nzT+~f>%qg@{BhgS`Xv85 z;SXWi-Eu!p_x&9(Vf3MGv$U8KLhjW9k6eiO1kn3@+8q17C9N5#Qm~5x>A}vXn>)Of zlp1FmL@7c~f~@<}mZ=Gzx+~?x$H_?E=o^HTm%-r>my(LdA%J_n)&1z$V*i=#E@oekPK?thJ>ou~pMVDIgay{p6% zw_4$hkodtiE-RhyFL4DSfxPxlmZx6u%9AqTvBvXoqpYpo#b!;0$EA(?n8^~C!p zC!86qmk=-Xo5cxAmH#<~m3EOTK2*4Isw;+by)&8?VlVI!XfwIIYPM@@rXESc4c&VKmNE-lg8^gq@~RnMGZmq4 zn1FV!;uOWj|Gk`We^I7sN}4QCgC29eJ>1iI�_W!$5!f0Y5OD|C$dD8XnI+6$|3Q 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 n&j0tFR{vL*a(wltd8$l%$77FPy7@l<{jf-R literal 0 HcmV?d00001 diff --git a/examples/impress_r/run_21736435_report.md b/examples/impress_r/run_21736435_report.md new file mode 100644 index 0000000..01e04ec --- /dev/null +++ b/examples/impress_r/run_21736435_report.md @@ -0,0 +1,156 @@ +# IMPRESS-R Run 21736435 — Report + +**Date:** 2026-09-02 +**Job:** 21736435 | `gpuA40x4` partition | node `gpub096` +**Wall time:** 6h | **Elapsed at report time:** ~1h +**Pipelines:** 16 (p1–p16) | **Max passes:** 10 | **Trainer:** ProteinMPNN (real fine-tune) + +--- + +## ROME-A Training + +### Status +- Training rounds completed at report time: **v14** (30 designs in corpus) +- Corpus grew from 0 → 78+ designs across 1h of runtime +- All rounds succeeded — weights published into `vanilla_model_weights/` after each round + +### Warning: Dragon future never resolves (recurring, non-fatal) + +Every training round emitted this warning: + +``` +[WARNING] [ROME-TRAINER] round .../train_complete: the execution backend has not +delivered a result after Xs, but the checkpoint is on disk — publishing from disk. +The task finished; its future never resolved, which on Dragon means a running service +is blocking result delivery (see docs/dragon.md). +``` + +**What it means:** `mpnn_train_wrapper.py` runs as a Dragon subprocess. Dragon delivers the task result via a future, but the IMPRESS event loop (a long-lived Dragon service) holds a ProcessGroup open, and Dragon's result-delivery path requires the ProcessGroup to be idle. ROME's fallback kicks in after `ROME_FALLBACK` seconds (60s default): it detects `train_complete` on disk and publishes the weights directly, bypassing the stuck future. + +**Impact:** None — all checkpoints were published correctly. Training proceeded normally. Every round v1–v14 completed this way. + +### Error: ProcessGroup state error (occurred once, round 6) + +``` +[ERROR] [ROME-TRAINER] training round failed: + RuntimeError: ProcessGroup manager is not in state State.DEAD +``` + +- **When:** 12:27:18, round v6 (14 designs) +- **What happened:** Dragon tried to reuse a ProcessGroup that hadn't fully torn down from the previous round. ROME caught the error, retried immediately with 16 designs (2 more had arrived in the meantime), and the retry succeeded. +- **Impact:** None — one round was delayed by ~2s, retry completed normally. + +### Training round timeline + +| Round | Designs | Dispatch time | Result | +|-------|---------|--------------|--------| +| v1 | 3 | 12:02:36 | OK (disk fallback, 34s) | +| v2 | 8 | 12:14:32 | OK (disk fallback, 36s) | +| v3 | 9 | 12:15:14 | OK (disk fallback, 38s) | +| v4 | 11 | 12:16:00 | OK (disk fallback, 70s) | +| v5 | 11 | 12:25:53 | OK (disk fallback, 32s) | +| v6 | 14 | 12:26:40 | **FAILED** → retry (16 designs) → OK | +| v7 | 16 | 12:36:19 | OK (disk fallback, 70s) | +| v8 | 18 | 12:37:42 | OK (disk fallback, 54s) | +| v9–v11 | 21–26 | ~12:40–12:50 | OK | +| v12 | 26 | 12:54:03 | Retried 3× (round 12 submitted 3 times) | +| v13 | 27 | 12:59:14 | OK | +| v14 | 30 | 13:00:41 | OK | + +> **Note on v12:** Round 12 was submitted three times in quick succession (12:54:03, 12:54:48, 12:55:39). This indicates the ProcessGroup error recurred and ROME retried twice before succeeding. The same pattern as v6 but with two retries. + +--- + +## pLDDT Statistics + +Computed from **289 CSV files** in `/scratch/bblj/mgoliyad1/IMPRESS_outputs/`. +One design per file (one protein × one pass). + +### Summary + +| Stat | avg_pLDDT | +|--------|-----------| +| Min | 42.51 | +| Max | 97.47 | +| Mean | 87.20 | +| Median | 91.40 | +| Stdev | 11.37 | + +### Percentiles + +| Percentile | avg_pLDDT | +|------------|-----------| +| p10 | 76.63 | +| p25 | 84.81 | +| p75 | 94.16 | +| p90 | 96.39 | + +### Threshold breakdown + +| Threshold | Count | Fraction | +|----------------|-------|----------| +| < 75 (low) | 27 | 9.3% | +| ≥ 75 (pass) | 262 | 90.7% | +| ≥ 90 (high) | 162 | 56.1% | + +**Interpretation:** 90.7% of designs passed the pLDDT ≥ 75 filter. The median of 91.4 and 56% above 90 indicate the pipeline is producing high-confidence Boltz predictions. The 27 sub-75 outliers are likely early-pass designs or cases where Boltz struggled with the binder geometry; the adaptive logic routes these proteins to child pipelines with a new sequence rank. + +--- + +## ROME Score Improvement Analysis + +Plots generated by `plot_rome_scores.py` (run at ~1h into the job, 289 design evaluations). +Re-run after job completion to capture the full 16 × 10 pass dataset. + +### Plot 1 — Per-pipeline trajectories + +![Per-pipeline avg_pLDDT over passes](rome_pipeline_trajectories.png) + +Root pipelines only (depth-0). Each line is one of the 16 pipelines tracked across its passes. +Key observations: +- **p13, p15** (≥95): naturally high-scoring targets — Boltz consistently confident regardless of sequence. +- **p2** (97 early → 85 at pass 5): sharp drop coincides with adaptive migration to child pipeline; root pipeline retained the harder sequence rank. +- **p7, p14** (42–63): consistently low — these targets appear structurally difficult for the binder geometry; candidates for early termination in future runs. +- Most pipelines cluster in the 82–94 range and show mild oscillation rather than monotone drift, which is expected when a single protein is evaluated per pipeline. + +### Plot 2 — Per-pass distribution (all depths) + +![avg_pLDDT distribution per pass](rome_pass_distribution.png) + +Box plot across all pipelines and child depths per pass. +Design count rises pass 1→3 as child pipelines spin up, then stabilises at 18 for passes 7–10 (only the deepest sub-pipelines still running). +The interquartile range narrows in later passes, consistent with selection pressure removing the lowest-scoring designs. + +### Plot 3 — Global mean trend + +![Global mean avg_pLDDT with ROME training markers](rome_global_trend.png) + +| Pass | Mean pLDDT | Median | n | +|------|-----------|--------|----| +| 1 | 85.87 | 90.47 | 16 | +| 2 | 86.24 | 91.81 | 44 | +| 3 | 87.55 | 91.04 | 47 | +| 4 | 87.20 | 90.14 | 38 | +| 5 | 87.15 | 90.78 | 40 | +| 6 | 86.74 | 91.58 | 34 | +| 7 | 87.95 | 92.17 | 18 | +| 8 | 88.23 | 92.31 | 18 | +| 9 | 88.30 | 92.39 | 18 | +| 10 | 88.52 | 91.99 | 18 | + +**Linear trend slope: +0.264 pLDDT / pass** — positive, consistent with ROME improving sequence quality over time. + +The signal is modest because: +1. Only 1h of data; most pipelines are still in early passes where ROME model versions v1–v5 are in effect. +2. Low-scoring targets (p7, p14) pull the early-pass mean down; they are absent from passes 7–10 (culled by adaptive logic), which artificially widens the late-pass mean. +3. The strongest ROME signal will appear in a side-by-side comparison of runs with `ROME_TRAINER=mpnn` vs `ROME_TRAINER=dummy` on the same input set. + +Vertical markers on plot 3 show ROME training events (v1–v8) placed proportionally along the time axis. + +--- + +## Next Steps + +1. **ROME ProcessGroup bug:** The `ProcessGroup manager is not in state State.DEAD` error recurred at v6 and v12. Investigate whether `mpnn_train_wrapper.py` needs an explicit `dist.destroy_process_group()` call at exit, or whether `ROME_FALLBACK` should be lowered to reduce the window between the stuck future and disk fallback. +2. **Post-run analysis:** When the job completes, re-run `plot_rome_scores.py` to capture all 16 pipelines × up to 10 passes and update this report. +3. **Control comparison:** Run an identical job with `ROME_TRAINER=dummy` to isolate ROME's contribution from the natural adaptive/selection effect. diff --git a/examples/impress_r/run_protein_binding_rome.py b/examples/impress_r/run_protein_binding_rome.py new file mode 100644 index 0000000..350ab52 --- /dev/null +++ b/examples/impress_r/run_protein_binding_rome.py @@ -0,0 +1,353 @@ +import copy +import csv +import os +import shutil +import asyncio +import tempfile +from typing import Dict, Any, Optional, List + +from rhapsody.backends import DragonExecutionBackend +from rhapsody.telemetry import define_event +from rhapsody.telemetry.events import make_event + +from impress import GPUPolicy, _find_gpus, _make_policy, ImpressManager, PipelineSetup + +try: + from examples.impress_r.protein_binding_rome import ProteinBindingPipeline +except ModuleNotFoundError: + import sys + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from protein_binding_rome import ProteinBindingPipeline + +try: + from examples.impress_r.mpnn import ( + ProteinMPNNConfig, + ProteinMPNNTrainer, + percentile_sampler, + ) +except ModuleNotFoundError: + import sys + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from mpnn import ProteinMPNNConfig, ProteinMPNNTrainer, percentile_sampler + +import rome + +import rhapsody, logging +rhapsody.enable_logging(level=logging.INFO) + + +# ── Test mode (IMPRESS_TEST_MODE=1) ─────────────────────────────────────────── +# Runs 2 pipelines with max_passes=1 and no child pipelines, so a single +# MPNN → Boltz → pLDDT → ROME cycle completes for integration testing. +TEST_MODE = os.getenv("IMPRESS_TEST_MODE", "0") == "1" +N_PIPELINES = 2 if TEST_MODE else 16 +MAX_PASSES = 1 if TEST_MODE else int(os.environ.get("ROME_MAX_PASSES", 10)) +MAX_SUB_PIPELINES_OVERRIDE = 0 if TEST_MODE else None # None = use inline default (3) + + +# ── ProteinMPNN repo ─────────────────────────────────────────────────────────── +# The same checkout IMPRESS runs for inference; ROME fine-tunes and publishes back +# into vanilla_model_weights/ so the next pass picks it up with no wrapper change. +MPNN_REPO = os.environ.get( + "ROME_MPNN_REPO", + os.environ.get("MPNN_PATH", ""), +) + + +# --------------------------------------------------------------------------- +# Custom telemetry events +# --------------------------------------------------------------------------- + +ProteinScore = define_event( + "impress.ProteinScore", + protein=str, + pipeline_name=str, + pass_num=int, + current_score=float, + previous_score=float, + decision=str, +) + +PassSummary = define_event( + "impress.PassSummary", + pipeline_name=str, + pass_num=int, + num_proteins=int, + num_degraded=int, + child_spawned=bool, +) + +ChildPipelineSpawned = define_event( + "impress.ChildPipelineSpawned", + parent_name=str, + child_name=str, + num_proteins=int, + seq_rank=int, +) + + +# --------------------------------------------------------------------------- +# ROME trainer builder +# --------------------------------------------------------------------------- + +def _build_trainer(checkpoint_dir: str): + """ProteinMPNN fine-tuner by default; a no-op dummy for smoke testing. + + ROME_TRAINER=mpnn → real ProteinMPNN fine-tune (needs ROME_MPNN_REPO) + ROME_TRAINER=dummy → smoke test (no GPU/torch required for training) + """ + want = os.environ.get("ROME_TRAINER", "mpnn").lower() + if want == "mpnn" and os.path.isdir(MPNN_REPO): + return ProteinMPNNTrainer(ProteinMPNNConfig( + mpnn_repo=MPNN_REPO, + initial_weights=os.path.join(MPNN_REPO, "vanilla_model_weights", "v_48_020.pt"), + model_name="v_48_020", + publish_into_repo=True, + ), gpus=1) + + if want == "mpnn": + print(f"[ROME-A] ROME_MPNN_REPO={MPNN_REPO!r} not found; " + "falling back to the dummy trainer (set ROME_MPNN_REPO or " + "ROME_TRAINER=dummy to silence this).") + from rome.dummy import DummyTrainer + return DummyTrainer(train_seconds=1.0, gpus=0) + + +# --------------------------------------------------------------------------- +# Real-time failure subscriber +# --------------------------------------------------------------------------- + +def _on_task_event(event) -> None: + if event.event_type == "TaskFailed": + wid = getattr(event, "workflow_id", None) + print(f"[TELEMETRY] TaskFailed task={event.task_id} workflow={wid}") + + +# --------------------------------------------------------------------------- +# Adaptive criteria +# --------------------------------------------------------------------------- + +def adaptive_criteria(current_score: float, previous_score: float) -> bool: + return current_score > previous_score + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +async def impress_protein_bind() -> None: + workdir = tempfile.mkdtemp(prefix="impress_r_") + stage_dir = os.path.join(workdir, "designs") + os.makedirs(stage_dir, exist_ok=True) + + # ROME-A gets its own Dragon backend so training tasks run in their own + # processes (separate CUDA context) independent of IMPRESS tasks. + rome_backend = await DragonExecutionBackend() + rome_manager = rome.Manager( + backend=rome_backend, + data_config=rome.DataConfig( + min_samples=int(os.environ.get("ROME_MIN_SAMPLES", 2)), + sample_func=percentile_sampler(0.33), + ), + trainer_config=rome.TrainerConfig( + trainer=_build_trainer(os.path.join(workdir, "checkpoints")), + checkpoint_dir=os.path.join(workdir, "checkpoints"), + poll_interval=1.0, + result_fallback_seconds=float(os.environ.get("ROME_FALLBACK", 60)), + ), + ) + await rome_manager.start() + + backend = await DragonExecutionBackend() + manager: ImpressManager = ImpressManager( + execution_backend=backend, + telemetry_config={ + "checkpoint_path": "./telemetry/", + "resource_poll_interval": 5.0, + }, + telemetry_subscribers=[_on_task_event], + ) + + # adaptive_decision closes over manager and rome_manager. + async def adaptive_decision(pipeline: ProteinBindingPipeline) -> Optional[Dict[str, Any]]: + MAX_SUB_PIPELINES: int = ( + MAX_SUB_PIPELINES_OVERRIDE if MAX_SUB_PIPELINES_OVERRIDE is not None else 3 + ) + tel = manager.telemetry + sid = tel.session_id if tel else None + + # Read CSV — written by s5_plddt_extract.sh to output_base_path. + file_name = os.path.join( + pipeline.output_base_path, + f"af_stats_{pipeline.name}_pass_{pipeline.passes}.csv", + ) + accepted = 0 + with open(file_name) as fd: + for row in csv.DictReader(fd): # ID, avg_plddt, ptm, avg_pae + protein = row["ID"].split(".")[0] + pipeline.current_scores[protein] = float(row["avg_pae"]) + + # -- ROME-A HOOK 1: contribute this design to the training corpus + src = os.path.join(pipeline.output_path_af, f"{protein}.pdb") + if not os.path.exists(src): + continue + staged = os.path.join( + stage_dir, + f"{pipeline.name}_pass{pipeline.passes}_{protein}.pdb", + ) + shutil.copyfile(src, staged) + ranked = pipeline.iter_seqs.get(protein) or [] + sequence = ranked[pipeline.seq_rank][0] if len(ranked) > pipeline.seq_rank else "" + uid = rome_manager.add_training_data( + path=staged, + sequence=sequence, + backbone_id=protein, + pLDDT=float(row["avg_plddt"]), + pTM=float(row["ptm"]), + pAE=float(row["avg_pae"]), + score=float(row["avg_plddt"]), + ) + accepted += uid is not None + + # -- ROME-A HOOK 2: collect the improved model for next pass + weights = rome_manager.get_current_model() + pipeline.logger.pipeline_log( + f"ROME-A: corpus {rome_manager.data.total_count} (+{accepted} this pass) | " + f"{rome_manager.get_training_status().name}" + + (f" | model {os.path.basename(weights)}" if weights else "") + ) + + # First pass — just save current scores as previous + if not pipeline.previous_scores: + pipeline.logger.pipeline_log("Saving current scores as previous and returning") + pipeline.previous_scores = copy.deepcopy(pipeline.current_scores) + return + + # Identify proteins that got worse (higher avg_pae = worse interface) + sub_iter_seqs: Dict[str, str] = {} + for protein, curr_score in pipeline.current_scores.items(): + if protein not in pipeline.iter_seqs: + continue + prev_score = pipeline.previous_scores[protein] + decision = adaptive_criteria(curr_score, prev_score) + pipeline.logger.pipeline_log(f"Adaptive decision: {decision}") + + if tel: + tel.emit(make_event( + ProteinScore, + session_id=sid, + backend="rhapsody", + protein=protein, + pipeline_name=pipeline.name, + pass_num=pipeline.passes, + current_score=curr_score, + previous_score=prev_score, + decision="degrade" if decision else "keep", + )) + + if decision: + sub_iter_seqs[protein] = pipeline.iter_seqs.pop(protein) + + # Spawn a new pipeline for degraded proteins + child_spawned = False + if sub_iter_seqs and pipeline.sub_order < MAX_SUB_PIPELINES: + new_name: str = f"{pipeline.name}_sub{pipeline.sub_order + 1}" + pipeline.set_up_new_pipeline_dirs(new_name) + + for protein in sub_iter_seqs: + src = f"{pipeline.output_path_af}/{protein}.pdb" + dst = f"{pipeline.input_base_path}/prod_in/{new_name}_in/{protein}.pdb" + shutil.copyfile(src, dst) + + if tel: + tel.emit(make_event( + ChildPipelineSpawned, + session_id=sid, + backend="rhapsody", + parent_name=pipeline.name, + child_name=new_name, + num_proteins=len(sub_iter_seqs), + seq_rank=pipeline.seq_rank + 1, + )) + + new_config = { + "name": new_name, + "type": type(pipeline), + "adaptive_fn": adaptive_decision, + "config": { + "is_child": True, + "start_pass": pipeline.passes, + "passes": pipeline.passes, + "iter_seqs": sub_iter_seqs, + "seq_rank": pipeline.seq_rank + 1, + "sub_order": pipeline.sub_order + 1, + "previous_scores": copy.deepcopy(pipeline.previous_scores), + "input_base_path": pipeline.input_base_path, + "output_base_path": pipeline.output_base_path, + }, + } + + pipeline.submit_child_pipeline_request(new_config) + pipeline.finalize(sub_iter_seqs) + + if not pipeline.fasta_list_2: + pipeline.kill_parent = True + + child_spawned = True + else: + pipeline.previous_scores = copy.deepcopy(pipeline.current_scores) + + if tel: + tel.emit(make_event( + PassSummary, + session_id=sid, + backend="rhapsody", + pipeline_name=pipeline.name, + pass_num=pipeline.passes, + num_proteins=len(pipeline.current_scores), + num_degraded=len(sub_iter_seqs), + child_spawned=child_spawned, + )) + + # Paths — same convention as protein_binding runner. + scripts_dir = os.environ.get( + "IMPRESS_SCRIPTS_DIR", os.path.dirname(os.path.abspath(__file__)) + ) + input_base_dir = os.environ.get("IMPRESS_BASE_DIR", scripts_dir) + output_base_dir = os.environ.get("IMPRESS_OUTPUT_DIR", scripts_dir) + os.makedirs(output_base_dir, exist_ok=True) + + all_gpus = _find_gpus() + + pipeline_setups: List[PipelineSetup] = [ + PipelineSetup( + name=f"p{str(i)}", + type=ProteinBindingPipeline, + config={ + "base_path": scripts_dir, + "input_base_path": input_base_dir, + "output_base_path": output_base_dir, + "policy": _make_policy(all_gpus, i - 1), + }, + adaptive_fn=adaptive_decision, + max_passes=MAX_PASSES, + ) + for i in range(1, N_PIPELINES + 1) + ] + + try: + await manager.start(pipeline_setups=pipeline_setups) + print("\nROME-A:", rome_manager.report()) + if manager.telemetry: + summary = manager.telemetry.summary() + print(f"[TELEMETRY] tasks={summary.get('tasks', {})}") + dur = summary.get("duration") + if dur: + print(f"[TELEMETRY] mean task time: {dur['mean_seconds'] * 1000:.1f} ms") + await manager.telemetry.stop() + finally: + await rome_manager.stop() + + +if __name__ == "__main__": + asyncio.run(impress_protein_bind()) diff --git a/examples/impress_r/scripts/s1_mpnn.sh b/examples/impress_r/scripts/s1_mpnn.sh new file mode 100755 index 0000000..b490f10 --- /dev/null +++ b/examples/impress_r/scripts/s1_mpnn.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +# Step 1: Sequence prediction via ProteinMPNN +# Args: $1=mpnn_script $2=input_path $3=output_dir $4=mpnn_path $5=num_seqs $6=chain + +mpnn_script="$1" +input_path="$2" +output_dir="$3" +mpnn_path="$4" +num_seqs="$5" +chain="$6" + +# Re-activate the IMPRESS venv inside Dragon tasks (VIRTUAL_ENV is exported by sbatch). +[ -n "${VIRTUAL_ENV:-}" ] && source "${VIRTUAL_ENV}/bin/activate" + +python3 "$mpnn_script" \ + -pdb="$input_path" \ + -out="$output_dir" \ + -mpnn="$mpnn_path" \ + -seqs="$num_seqs" \ + -is_monomer=0 \ + -chains="$chain" diff --git a/examples/impress_r/scripts/s4_boltz.sh b/examples/impress_r/scripts/s4_boltz.sh new file mode 100755 index 0000000..a10cef9 --- /dev/null +++ b/examples/impress_r/scripts/s4_boltz.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e + +# Step 4: Structure prediction via Boltz +# Args: $1=fasta_path $2=output_dir + +fasta_path="$1" +output_dir="$2" + +# Boltz requires Python <=3.12 (numpy<2.0 etc.) so it lives in its own env. +# BOLTZ_VENV may point to a conda env (no bin/activate) or a pip venv; prepend +# its bin/ to PATH so the correct python/boltz are found in either case. +_BOLTZ_ENV="${BOLTZ_VENV:-${VIRTUAL_ENV:-}}" +[ -n "${_BOLTZ_ENV}" ] && export PATH="${_BOLTZ_ENV}/bin:${PATH}" +export SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt + +# Compute nodes typically have no internet access, so --use_msa_server is off +# by default. Set BOLTZ_USE_MSA_SERVER=1 to enable it on nodes with internet. +_MSA_FLAG="" +[ "${BOLTZ_USE_MSA_SERVER:-0}" = "1" ] && _MSA_FLAG="--use_msa_server" + +# ── Test-mode stub (IMPRESS_TEST_MODE=1) ────────────────────────────────── +# Write minimal Boltz-shaped output so downstream steps (plddt_extract_pipeline) +# can run without invoking the real Boltz model. numpy is available because +# BOLTZ_VENV/bin is already on PATH above. +if [ "${IMPRESS_TEST_MODE:-0}" = "1" ]; then + name="$(basename "${fasta_path}" .fa)" + pred_dir="${output_dir}/boltz_results_${name}/predictions/${name}" + mkdir -p "${pred_dir}" + python3 - "${pred_dir}" "${name}" <<'PYEOF' +import sys, json +import numpy as np +pred_dir, name = sys.argv[1], sys.argv[2] +n = 110 # 100 PDZ residues + 10 peptide (PEP_LEN=10 assumed by extractor) +np.savez(f"{pred_dir}/plddt_{name}_model_0.npz", plddt=np.full(n, 0.85)) +np.savez(f"{pred_dir}/pae_{name}_model_0.npz", pae=np.full((n, n), 2.0)) +with open(f"{pred_dir}/confidence_{name}_model_0.json", "w") as f: + json.dump({"iptm": 0.75, "ptm": 0.80}, f) +PYEOF + echo "[MOCK] s4_boltz stub done for ${name}" + exit 0 +fi +# ── End test-mode stub ──────────────────────────────────────────────────── + +mkdir -p "${output_dir}" + +# Prevent PyTorch Lightning from installing SLURM auto-requeue signal handlers. +# PL detects SLURM_JOB_ID and registers SIGTERM/SIGUSR handlers that keep the +# process group alive after Boltz finishes, causing Dragon to report +# "ProcessGroup manager is not in state State.DEAD". +unset SLURM_JOB_ID + +boltz predict \ + "${fasta_path}" \ + --out_dir "${output_dir}" \ + ${_MSA_FLAG} \ + --cache "${BOLTZ_CACHE_DIR:-${HOME}/.boltz}" \ + --output_format pdb \ + --write_full_pae \ + --no_kernels \ + --devices 1 \ + --override \ + 2>&1 | tee "${output_dir}/boltz_run.log" +# tee exits 0; check the actual boltz exit code via PIPESTATUS +test "${PIPESTATUS[0]}" -eq 0 diff --git a/examples/impress_r/scripts/s5_plddt_extract.sh b/examples/impress_r/scripts/s5_plddt_extract.sh new file mode 100755 index 0000000..11a3367 --- /dev/null +++ b/examples/impress_r/scripts/s5_plddt_extract.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +# Step 5: pLDDT extraction +# Args: $1=output_base_path $2=iter $3=out_name + +output_base_path="$1" +iter="$2" +out_name="$3" + +# plddt_extract_pipeline.py lives one level above this scripts/ directory. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Re-activate the IMPRESS venv inside Dragon tasks (VIRTUAL_ENV is exported by sbatch). +[ -n "${VIRTUAL_ENV:-}" ] && source "${VIRTUAL_ENV}/bin/activate" + +python3 "${SCRIPT_DIR}/../plddt_extract_pipeline.py" \ + --path="${output_base_path}" \ + --iter="${iter}" \ + --out="${out_name}" From 2af8ba51f36e6281fa5a51bb9902b43ef628cc8b Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Wed, 2 Sep 2026 14:04:11 -0500 Subject: [PATCH 08/12] docs: expand score analysis to per-depth breakdown and update report Plot 3 now shows depth-stratified mean trajectories (root/sub1/sub2/sub3) instead of a single global mean, surfacing the ROME signal more clearly. Report updated with final 296-design stats, per-depth table, and revised key observations. plot_rome_scores.py updated to match. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DbNxkFCwkCEEkHnN8xGH7Q --- examples/impress_r/plot_rome_scores.py | 93 ++++++++------ examples/impress_r/rome_global_trend.png | Bin 107484 -> 159037 bytes examples/impress_r/rome_pass_distribution.png | Bin 46170 -> 46344 bytes examples/impress_r/run_21736435_report.md | 118 +++++++++++------- 4 files changed, 127 insertions(+), 84 deletions(-) diff --git a/examples/impress_r/plot_rome_scores.py b/examples/impress_r/plot_rome_scores.py index 9426ad2..d9b836e 100644 --- a/examples/impress_r/plot_rome_scores.py +++ b/examples/impress_r/plot_rome_scores.py @@ -21,6 +21,7 @@ import re import statistics from collections import defaultdict +from itertools import groupby import matplotlib matplotlib.use("Agg") @@ -57,6 +58,11 @@ if vals: records.append((root, pipeline, depth, pass_num, statistics.mean(vals), vals)) +# depth_pass: (depth, pass_num) -> [values] — used for depth-stratified plot +depth_pass = defaultdict(list) +for root, pipeline, depth, pass_num, mean, vals in records: + depth_pass[(depth, pass_num)].extend(vals) + # ── Load ROME training events from log ─────────────────────────────────────── # Extract unique (round, timestamp) from log lines like: # 12:02:36.532 [INFO] [ROME-TRAINER] submitting training round 1 (3 designs ...) -> v1 @@ -159,53 +165,54 @@ print(f"Saved: {out2}") # ═══════════════════════════════════════════════════════════════════════════════ -# Plot 3 — Global mean per pass + regression + ROME round markers +# Plot 3 — Mean per pass stratified by sub-pipeline depth + global trend # ═══════════════════════════════════════════════════════════════════════════════ +DEPTH_COLORS = {0: "#4C72B0", 1: "#DD8452", 2: "#55A868", 3: "#C44E52"} +DEPTH_LABELS = {0: "depth 0 (root)", 1: "depth 1 (sub1)", + 2: "depth 2 (sub1_sub2)", 3: "depth 3 (sub1_sub2_sub3)"} + +fig3, ax3 = plt.subplots(figsize=(12, 6)) + +all_depths = sorted({d for d, p in depth_pass}) +for depth in all_depths: + xs = sorted(p for d, p in depth_pass if d == depth) + ys = [statistics.mean(depth_pass[(depth, p)]) for p in xs] + ns = [len(depth_pass[(depth, p)]) for p in xs] + color = DEPTH_COLORS.get(depth, "#888888") + ax3.plot(xs, ys, marker="o", markersize=5, linewidth=1.8, + color=color, label=DEPTH_LABELS.get(depth, f"depth {depth}"), zorder=3) + for x, y, n in zip(xs, ys, ns): + ax3.text(x, y + 0.5, f"{y:.1f}", ha="center", fontsize=6.5, + color=color, alpha=0.85) + +# Global mean + regression pass_means = {p: statistics.mean(v) for p, v in pass_vals.items()} xs_all = sorted(pass_means) ys_all = [pass_means[x] for x in xs_all] - -# Linear regression over the mean trajectory coef = np.polyfit(xs_all, ys_all, 1) trend_y = np.polyval(coef, xs_all) -fig3, ax3 = plt.subplots(figsize=(11, 6)) +ax3.plot(xs_all, ys_all, linewidth=2.2, linestyle="--", + color=ACCENT, alpha=0.6, + label=f"global mean (slope {coef[0]:+.3f}/pass)", zorder=2) -ax3.plot(xs_all, ys_all, marker="o", markersize=6, linewidth=2, - color=ACCENT, label="global mean avg_pLDDT", zorder=3) -ax3.plot(xs_all, trend_y, linewidth=1.2, linestyle="--", - color="#888888", label=f"trend (slope {coef[0]:+.3f}/pass)", zorder=2) -ax3.fill_between(xs_all, - [statistics.mean(pass_vals[p]) - statistics.stdev(pass_vals[p]) if len(pass_vals[p]) > 1 else pass_means[p] for p in xs_all], - [statistics.mean(pass_vals[p]) + statistics.stdev(pass_vals[p]) if len(pass_vals[p]) > 1 else pass_means[p] for p in xs_all], - alpha=0.12, color=ACCENT, label="±1 stdev") - -# Add per-pass mean labels -for x, y in zip(xs_all, ys_all): - ax3.text(x, y + 0.6, f"{y:.1f}", ha="center", fontsize=8, color=ACCENT) - -ax3.set_xlabel("Pass", fontsize=11) -ax3.set_ylabel("Mean avg_pLDDT", fontsize=11) -ax3.set_title("Global mean avg_pLDDT per pass with ROME training events", fontsize=13, pad=10) -ax3.set_ylim(60, 100) -ax3.axhline(75, color="#BBBBBB", linewidth=0.8, linestyle=":") - -# ROME round markers on a secondary x-axis band (minutes since start) +# ROME round markers if rome_rounds: - ax3b = ax3.twiny() - ax3b.set_xlim(ax3.get_xlim()) - # Map ROME round to pass by aligning their timing with pass indices - # Annotate as vertical bands on the main axis (approximated) - for rnd in sorted(rome_rounds)[:8]: # show first 8 to avoid clutter - # Place marker between pass 1 and max pass proportionally - t_frac = rome_rounds[rnd] / max(rome_rounds.values()) if max(rome_rounds.values()) > 0 else 0 + max_t = max(rome_rounds.values()) + for rnd in sorted(rome_rounds)[:10]: + t_frac = rome_rounds[rnd] / max_t if max_t > 0 else 0 x_pos = xs_all[0] + t_frac * (xs_all[-1] - xs_all[0]) - ax3.axvline(x_pos, color="#99AACC", linewidth=0.6, linestyle=":", alpha=0.7) - ax3.text(x_pos, 61.5, f"v{rnd}", fontsize=6.5, color="#667799", + ax3.axvline(x_pos, color="#99AACC", linewidth=0.55, linestyle=":", alpha=0.65) + ax3.text(x_pos, 79.5, f"v{rnd}", fontsize=6, color="#667799", ha="center", rotation=90, va="bottom") - ax3b.set_visible(False) -ax3.legend(fontsize=9, loc="lower left") +ax3.set_xlabel("Pass", fontsize=11) +ax3.set_ylabel("Mean avg_pLDDT", fontsize=11) +ax3.set_title("avg_pLDDT per pass by sub-pipeline depth (ROME training markers in blue)", fontsize=13, pad=10) +ax3.set_ylim(78, 100) +ax3.axhline(75, color="#CCCCCC", linewidth=0.7, linestyle=":") +ax3.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) +ax3.legend(fontsize=9, loc="lower right") fig3.tight_layout() out3 = os.path.join(args.out_dir, "rome_global_trend.png") @@ -213,17 +220,23 @@ print(f"Saved: {out3}") # ── Text summary ────────────────────────────────────────────────────────────── -print("\n=== Per-pass global mean ===") +print("\n=== Per-pass global mean (all depths) ===") +print(f"{'Pass':>4} {'Mean':>7} {'Median':>7} {'n (designs)':>11}") for p in xs_all: vals = pass_vals[p] - print(f" pass {p:2d} mean={statistics.mean(vals):.2f} " - f"median={statistics.median(vals):.2f} n={len(vals)}") + print(f" {p:2d} {statistics.mean(vals):7.2f} {statistics.median(vals):7.2f} {len(vals):>11}") + +print("\n=== Per-depth per-pass mean ===") +print(f"{'Depth':>5} {'Pass':>4} {'Mean':>7} {'Median':>7} {'n':>4}") +for (depth, pass_num) in sorted(depth_pass): + vals = depth_pass[(depth, pass_num)] + print(f" {depth:3d} {pass_num:2d} {statistics.mean(vals):7.2f} {statistics.median(vals):7.2f} {len(vals):>4}") slope = coef[0] -print(f"\nLinear trend slope: {slope:+.4f} pLDDT / pass") +print(f"\nGlobal linear trend slope: {slope:+.4f} pLDDT / pass") if slope > 0.2: print(" → Positive trend: ROME appears to be improving sequence quality.") elif slope < -0.2: print(" → Negative trend: scores declining — may reflect harder proteins in later passes.") else: - print(" → Flat trend: no clear improvement signal yet (expected early in a run).") + print(" → Flat trend: no clear improvement signal yet.") diff --git a/examples/impress_r/rome_global_trend.png b/examples/impress_r/rome_global_trend.png index 0f1ff0c4c5d9d81f494739e5bbe0bcd1882291b2..cc788e657102a41b9342afca724618d2cc9c2f24 100644 GIT binary patch literal 159037 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�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

    (*LDN}#5;e)vagtBZ{O$rSF_gWth1 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 OOQy=sZJe!pPyQPQQ!M@f literal 107484 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

      1hSJWFMIFOh1chXE3vnk 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 Q7sA8Xqp~~Yx5H=uFN^p|UjP6A diff --git a/examples/impress_r/rome_pass_distribution.png b/examples/impress_r/rome_pass_distribution.png index dd1e6fcc55fb7fcdb82642d0fea55c9c118baa7f..63efe241de71fa6a4800f02b38d9e94838597af7 100644 GIT binary patch literal 46344 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 A`2YX_ literal 46170 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 P+&|^8;-M7Tlb8PwOL|$a diff --git a/examples/impress_r/run_21736435_report.md b/examples/impress_r/run_21736435_report.md index 01e04ec..d8a0c36 100644 --- a/examples/impress_r/run_21736435_report.md +++ b/examples/impress_r/run_21736435_report.md @@ -2,7 +2,7 @@ **Date:** 2026-09-02 **Job:** 21736435 | `gpuA40x4` partition | node `gpub096` -**Wall time:** 6h | **Elapsed at report time:** ~1h +**Wall time:** 6h | **Status:** COMPLETED **Pipelines:** 16 (p1–p16) | **Max passes:** 10 | **Trainer:** ProteinMPNN (real fine-tune) --- @@ -63,44 +63,43 @@ is blocking result delivery (see docs/dragon.md). ## pLDDT Statistics -Computed from **289 CSV files** in `/scratch/bblj/mgoliyad1/IMPRESS_outputs/`. -One design per file (one protein × one pass). +Computed from **296 CSV files** in `/scratch/bblj/mgoliyad1/IMPRESS_outputs/`. +One design per file (one protein × one pass). Final numbers after job completion. ### Summary | Stat | avg_pLDDT | |--------|-----------| | Min | 42.51 | -| Max | 97.47 | -| Mean | 87.20 | -| Median | 91.40 | -| Stdev | 11.37 | +| Max | 97.49 | +| Mean | 87.36 | +| Median | 91.81 | +| Stdev | 11.30 | ### Percentiles | Percentile | avg_pLDDT | |------------|-----------| -| p10 | 76.63 | -| p25 | 84.81 | -| p75 | 94.16 | -| p90 | 96.39 | +| p10 | 76.25 | +| p25 | 84.98 | +| p75 | 94.21 | +| p90 | 96.44 | ### Threshold breakdown | Threshold | Count | Fraction | |----------------|-------|----------| -| < 75 (low) | 27 | 9.3% | -| ≥ 75 (pass) | 262 | 90.7% | -| ≥ 90 (high) | 162 | 56.1% | +| < 75 (low) | 28 | 9.5% | +| ≥ 75 (pass) | 268 | 90.5% | +| ≥ 90 (high) | 168 | 56.8% | -**Interpretation:** 90.7% of designs passed the pLDDT ≥ 75 filter. The median of 91.4 and 56% above 90 indicate the pipeline is producing high-confidence Boltz predictions. The 27 sub-75 outliers are likely early-pass designs or cases where Boltz struggled with the binder geometry; the adaptive logic routes these proteins to child pipelines with a new sequence rank. +**Interpretation:** 90.5% of designs passed the pLDDT ≥ 75 filter. The median of 91.8 and 56.8% above 90 indicate the pipeline is producing high-confidence Boltz predictions. The 28 sub-75 outliers are concentrated in p7 and p14 (structurally difficult targets that stayed low throughout all passes); the adaptive logic routes these proteins to child pipelines with a new sequence rank but cannot fully overcome the target difficulty. --- ## ROME Score Improvement Analysis -Plots generated by `plot_rome_scores.py` (run at ~1h into the job, 289 design evaluations). -Re-run after job completion to capture the full 16 × 10 pass dataset. +Plots generated by `plot_rome_scores.py` on the full completed dataset (296 design evaluations, 16 pipelines × up to 10 passes). ### Plot 1 — Per-pipeline trajectories @@ -121,36 +120,67 @@ Box plot across all pipelines and child depths per pass. Design count rises pass 1→3 as child pipelines spin up, then stabilises at 18 for passes 7–10 (only the deepest sub-pipelines still running). The interquartile range narrows in later passes, consistent with selection pressure removing the lowest-scoring designs. -### Plot 3 — Global mean trend - -![Global mean avg_pLDDT with ROME training markers](rome_global_trend.png) - -| Pass | Mean pLDDT | Median | n | -|------|-----------|--------|----| -| 1 | 85.87 | 90.47 | 16 | -| 2 | 86.24 | 91.81 | 44 | -| 3 | 87.55 | 91.04 | 47 | -| 4 | 87.20 | 90.14 | 38 | -| 5 | 87.15 | 90.78 | 40 | -| 6 | 86.74 | 91.58 | 34 | -| 7 | 87.95 | 92.17 | 18 | -| 8 | 88.23 | 92.31 | 18 | -| 9 | 88.30 | 92.39 | 18 | -| 10 | 88.52 | 91.99 | 18 | - -**Linear trend slope: +0.264 pLDDT / pass** — positive, consistent with ROME improving sequence quality over time. - -The signal is modest because: -1. Only 1h of data; most pipelines are still in early passes where ROME model versions v1–v5 are in effect. -2. Low-scoring targets (p7, p14) pull the early-pass mean down; they are absent from passes 7–10 (culled by adaptive logic), which artificially widens the late-pass mean. -3. The strongest ROME signal will appear in a side-by-side comparison of runs with `ROME_TRAINER=mpnn` vs `ROME_TRAINER=dummy` on the same input set. - -Vertical markers on plot 3 show ROME training events (v1–v8) placed proportionally along the time axis. +### Plot 3 — Mean per pass by sub-pipeline depth + +![avg_pLDDT per pass by sub-pipeline depth with ROME training markers](rome_global_trend.png) + +Each line is one sub-pipeline depth level; dashed orange = global mean across all depths. Vertical dotted blue markers = ROME training round completions (v1–v10). + +#### Global mean (all depths combined) + +| Pass | Mean pLDDT | Median | n (designs) | +|------|-----------|--------|-------------| +| 1 | 85.87 | 90.47 | 16 | +| 2 | 86.24 | 91.81 | 44 | +| 3 | 87.55 | 91.04 | 47 | +| 4 | 87.36 | 90.18 | 39 | +| 5 | 87.56 | 91.04 | 42 | +| 6 | 86.87 | 92.08 | 35 | +| 7 | 88.44 | 92.40 | 19 | +| 8 | 88.23 | 92.31 | 18 | +| 9 | 88.30 | 92.39 | 18 | +| 10 | 88.52 | 91.99 | 18 | + +**Global linear trend slope: +0.268 pLDDT / pass** + +#### Per-depth breakdown + +| Depth | Pass | Mean pLDDT | Median | n (designs) | +|-------|------|-----------|--------|-------------| +| 0 (root) | 1 | 85.87 | 90.47 | 16 | +| 0 | 2 | 85.63 | 90.19 | 16 | +| 0 | 3 | 86.14 | 88.27 | 16 | +| 0 | 4 | 85.16 | 89.27 | 16 | +| 0 | 5 | 84.47 | 89.01 | 16 | +| 0 | 6 | 84.37 | 87.00 | 16 | +| 1 (sub1) | 2 | 86.70 | 91.81 | 12 | +| 1 | 3 | 87.14 | 89.77 | 10 | +| 1 | 4 | 88.95 | 88.26 | 5 | +| 1 | 5 | 89.03 | 89.62 | 5 | +| 2 (sub1_sub2) | 2 | 87.55 | 92.54 | 8 | +| 2 | 3 | 91.27 | 93.63 | 9 | +| 2 | 4 | 91.59 | 92.78 | 5 | +| 2 | 5 | 91.60 | 91.86 | 6 | +| 2 | 6 | 91.94 | 91.94 | 2 | +| 3 (sub1_sub2_sub3) | 2 | 85.47 | 92.31 | 8 | +| 3 | 3 | 86.97 | 90.55 | 12 | +| 3 | 4 | 87.84 | 92.80 | 13 | +| 3 | 5 | 88.75 | 92.78 | 15 | +| 3 | 6 | 88.68 | 93.06 | 16 | +| 3 | 7 | 88.56 | 92.55 | 16 | +| 3 | 8 | 88.64 | 92.45 | 16 | +| 3 | 9 | 88.71 | 92.74 | 16 | +| 3 | 10 | 88.96 | 92.39 | 16 | + +**Key observations:** +- **Depth 0 (root)** declines slightly (85.9 → 84.4) — the adaptive logic continuously migrates the best-performing proteins out into sub-pipelines, leaving the harder cases at the root. +- **Depth 2 (sub1_sub2)** shows the strongest absolute scores (87.6 → 91.9), consistently the highest-quality group across all passes. +- **Depth 3 (sub1_sub2_sub3)** shows the clearest ROME improvement signal: steady rise from 85.5 at pass 2 to 89.0 at pass 10 (+3.5 pLDDT), covering the most ROME training rounds (v2–v14+). +- The global mean conflates depth effects with ROME effects; the per-depth view isolates them. --- ## Next Steps 1. **ROME ProcessGroup bug:** The `ProcessGroup manager is not in state State.DEAD` error recurred at v6 and v12. Investigate whether `mpnn_train_wrapper.py` needs an explicit `dist.destroy_process_group()` call at exit, or whether `ROME_FALLBACK` should be lowered to reduce the window between the stuck future and disk fallback. -2. **Post-run analysis:** When the job completes, re-run `plot_rome_scores.py` to capture all 16 pipelines × up to 10 passes and update this report. -3. **Control comparison:** Run an identical job with `ROME_TRAINER=dummy` to isolate ROME's contribution from the natural adaptive/selection effect. +2. **Control comparison:** Run an identical job with `ROME_TRAINER=dummy` to isolate ROME's contribution from the natural adaptive/selection effect. The +0.268/pass slope currently conflates ROME improvement with selection bias (harder targets culled by later passes). From 3c3eeff315810b9bb91e8ad190b54af56a4f007c Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Thu, 3 Sep 2026 21:07:07 -0500 Subject: [PATCH 09/12] fix: Delta HPC env setup and pipeline fixes - delta_env_setup.sh (all examples): source lmod in non-interactive bash, use `module load python` for Python detection, fix version formula (major*100+minor), add matplotlib, export VIRTUAL_ENV before PyRosetta installer, pin dragonhpc==0.14.1 (0.14.2 breaks on Delta runtime), remove hardcoded bblj allocation from MINIFORGE and _scratch fallbacks - delta_gpu_run.sh: point default IMPRESS_VENV to correct per-example venv - impress_r/plot_rome_scores.py: remove hardcoded bblj allocation from SCRATCH fallback; require --csv-dir when SCRATCH is unset - small_molecule_binding/small_molecule_binding.py: prefix scaffold_arg with + for Hydra append semantics (scaffoldguided.target_pdb) - small_molecule_binding/delta_env_setup.sh: wrap COLABFOLD_CACHE_DIR in pathlib.Path() for download_alphafold_params Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DbNxkFCwkCEEkHnN8xGH7Q --- examples/impress_r/delta_env_setup.sh | 74 ++++++++++--------- examples/impress_r/delta_gpu_run.sh | 2 +- examples/impress_r/plot_rome_scores.py | 11 ++- examples/protein_binding/delta_env_setup.sh | 66 +++++++++-------- examples/protein_binding/delta_gpu_run.sh | 2 +- .../small_molecule_binding/delta_env_setup.sh | 59 ++++++++------- .../small_molecule_binding/delta_gpu_run.sh | 2 +- .../small_molecule_binding.py | 2 +- 8 files changed, 114 insertions(+), 104 deletions(-) diff --git a/examples/impress_r/delta_env_setup.sh b/examples/impress_r/delta_env_setup.sh index 7870f28..9b59674 100755 --- a/examples/impress_r/delta_env_setup.sh +++ b/examples/impress_r/delta_env_setup.sh @@ -12,12 +12,18 @@ # ENV_DIR = /u/$USER/ve/impress # IMPRESS_DIR = $SCRATCH/$USER/IMPRESS # ROME_DIR = $SCRATCH/$USER/ROME -# python = auto-detected (python/3.11, cray-python/3.11.7, anaconda3) +# python = auto-detected via `module load python` (Delta default: 3.13+) # ============================================================================= if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then set -euo pipefail fi +# ── Initialize lmod (needed when run as non-interactive bash script) ────────── +if ! declare -f module &>/dev/null; then + _lmod_init=/usr/share/lmod/lmod/init/bash + [ -f "${_lmod_init}" ] && source "${_lmod_init}" +fi + # ── Require SCRATCH ─────────────────────────────────────────────────────────── if [[ -z "${SCRATCH:-}" ]]; then echo "ERROR: set the SCRATCH env var to your allocation scratch root, e.g.:" @@ -27,7 +33,7 @@ if [[ -z "${SCRATCH:-}" ]]; then fi # ── Defaults / arg parsing ──────────────────────────────────────────────────── -ENV_DIR="${ENV_DIR:-/u/${USER}/ve/impress}" +ENV_DIR="${ENV_DIR:-/u/${USER}/ve/impress_A}" IMPRESS_DIR="${IMPRESS_DIR:-${SCRATCH}/${USER}/IMPRESS}" ROME_DIR="${ROME_DIR:-${SCRATCH}/${USER}/ROME}" BASE_PY_OVERRIDE="" @@ -55,34 +61,22 @@ echo "=================================================================" echo "" echo "── Step 1: Creating venv ──" -_find_python() { - for candidate in python3.12 python3.11 python3 python; do - local p - p=$(command -v "${candidate}" 2>/dev/null) || continue - local ver - ver=$("${p}" -c "import sys; v=sys.version_info; print(v.major*10+v.minor)" 2>/dev/null) || continue - [ "${ver}" -ge 311 ] && echo "${p}" && return 0 - done - return 1 -} - if [ -n "${BASE_PY_OVERRIDE}" ]; then BASE_PY="${BASE_PY_OVERRIDE}" echo "Using Python override: ${BASE_PY}" else - BASE_PY=$(_find_python || true) + # On Delta, `module load python` gives the default Python 3.13+. + module load python 2>/dev/null || true + BASE_PY=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true) if [ -z "${BASE_PY}" ]; then - echo "python3.11+ not in PATH — trying modules..." - for mod in python/3.12 python/3.11 cray-python/3.11.7 anaconda3; do - module load "${mod}" 2>/dev/null || true - BASE_PY=$(_find_python || true) - [ -n "${BASE_PY}" ] && echo " loaded module: ${mod}" && break - done + echo "ERROR: no Python found after 'module load python'." + echo " Pass an explicit interpreter: --python /path/to/python3" + exit 1 fi - if [ -z "${BASE_PY}" ]; then - echo "ERROR: no Python 3.11+ interpreter found." - echo " Pass an explicit interpreter: --python /path/to/python3.11" - echo " Or load a module manually before running this script." + ver=$("${BASE_PY}" -c "import sys; v=sys.version_info; print(v.major*100+v.minor)") + if [ "${ver}" -lt 311 ]; then + echo "ERROR: ${BASE_PY} is Python ${ver} — need 3.11+." + echo " Pass an explicit interpreter: --python /path/to/python3.11" exit 1 fi fi @@ -111,6 +105,10 @@ echo "── Step 3: radical-asyncflow (PyPI) ──" echo "" echo "── Step 4: rhapsody-py[dragon] (PyPI) ──" "${PIP}" install -q "rhapsody-py[dragon,telemetry]" +# Pin dragonhpc to 0.14.1 — 0.14.2 added waitForKeys to DDRegisterClientResponse +# but the Delta system Dragon runtime has not been updated to match; 0.14.2 fails +# with AttributeError on every DDict operation on this cluster. +"${PIP}" install -q "dragonhpc==0.14.1" # ── 5. IMPRESS (local editable) ─────────────────────────────────────────────── echo "" @@ -124,7 +122,7 @@ if [ -d "${ROME_DIR}" ]; then "${PIP}" install -q -e "${ROME_DIR}" else echo "WARNING: ROME_DIR=${ROME_DIR} not found — skipping ROME install." - echo " Set --rome-dir or clone ROME before running impress_r." + echo " Set --rome-dir or clone ROME before running impress_A." fi # ── 7. PyTorch (CUDA 12.1) ─────────────────────────────────────────────────── @@ -134,23 +132,27 @@ echo "── Step 7: PyTorch (cu121) ──" # ── 8. Additional dependencies ─────────────────────────────────────────────── echo "" -echo "── Step 8: pandas + biopandas ──" -"${PIP}" install -q pandas biopandas +echo "── Step 8: pandas + biopandas + matplotlib ──" +"${PIP}" install -q pandas biopandas matplotlib -# ── 9. PyRosetta (via pyrosetta-installer) ─────────────────────────────────── +# ── 9. PyRosetta ───────────────────────────────────────────────────────────── echo "" echo "── Step 9: PyRosetta ──" +# pyrosetta_installer handles credential lookup internally. +# Activate the venv in the environment so its subprocess pip installs there. +export VIRTUAL_ENV="${ENV_DIR}" +export PATH="${ENV_DIR}/bin:${PATH}" "${PIP}" install -q pyrosetta-installer "${PY}" -c "import pyrosetta_installer; pyrosetta_installer.install_pyrosetta()" -# ── 10. Boltz (structure prediction — separate Python 3.12 conda env) ──────── +# ── 10. Boltz (structure prediction — separate conda env) ──────────────────── echo "" echo "── Step 10: Boltz (separate conda env) ──" -# boltz 2.x requires numpy<2.0, scipy==1.13.1, etc. — none have Python 3.13 -# wheels, so boltz cannot be installed in the main Python 3.13 IMPRESS venv. -# Create a dedicated Python 3.12 conda env and install boltz there. +# boltz 2.x is kept in a separate conda env so its dependency pins (scipy, etc.) +# don't constrain the main IMPRESS venv. Boltz 2.0+ also installs fine via pip +# in Python 3.13, but we keep the separation to avoid pin conflicts. # s4_boltz.sh activates BOLTZ_VENV (set in delta_gpu_run.sh) instead of VIRTUAL_ENV. -MINIFORGE="${MINIFORGE:-/scratch/bblj/${USER}/miniforge3}" +MINIFORGE="${MINIFORGE:-$SCRATCH/${USER}/miniforge3}" BOLTZ_ENV="${BOLTZ_ENV:-${HOME}/ve/boltz}" # Cache lives in home dir — scratch inode quota can't hold the 45K CCD files. BOLTZ_CACHE="${BOLTZ_CACHE:-${HOME}/boltz}" @@ -183,7 +185,7 @@ echo " Boltz cache pre-warm done (model weights cached at ${BOLTZ_CACHE})" BOLTZ_MSA_CACHE="${BOLTZ_CACHE}/msa_cache" mkdir -p "${BOLTZ_MSA_CACHE}" echo " Pre-computing MSAs into ${BOLTZ_MSA_CACHE}" -_scratch="${SCRATCH:-/scratch/bblj/${USER}}" +_scratch="${SCRATCH}" _base_dir="${IMPRESS_BASE_DIR:-${_scratch}/IMPRESS_inputs}" _out_dir="${IMPRESS_OUTPUT_DIR:-${_scratch}/IMPRESS_outputs}" _msa_inputs_dir="${_base_dir}/prod_in" @@ -253,8 +255,8 @@ echo " source ${ENV_DIR}/bin/activate" echo "" echo "Run the pipeline:" echo " export SCRATCH=${SCRATCH}" -echo " export SBATCH_ACCOUNT=bblj-delta-gpu" -echo " cd ${IMPRESS_DIR}/examples/impress_r" +echo " export SBATCH_ACCOUNT=xxx-delta-gpu" +echo " cd ${IMPRESS_DIR}/examples/impress_A" echo " sbatch delta_gpu_run.sh" echo "" echo "Test (smoke) run:" diff --git a/examples/impress_r/delta_gpu_run.sh b/examples/impress_r/delta_gpu_run.sh index 549c0aa..7b66ed9 100755 --- a/examples/impress_r/delta_gpu_run.sh +++ b/examples/impress_r/delta_gpu_run.sh @@ -60,7 +60,7 @@ export FAB_LIB=/opt/cray/libfabric/1.22.0/lib64 export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH:-} # ── Environment ─────────────────────────────────────────────────────────────── -IMPRESS_VENV="${IMPRESS_VENV:-${HOME}/ve/impress}" +IMPRESS_VENV="${IMPRESS_VENV:-${HOME}/ve/impress_A}" unset SLURM_EXPORT_ENV source "${IMPRESS_VENV}/bin/activate" dragon-config add --ofi-runtime-lib="${FAB_LIB}" diff --git a/examples/impress_r/plot_rome_scores.py b/examples/impress_r/plot_rome_scores.py index d9b836e..e60082f 100644 --- a/examples/impress_r/plot_rome_scores.py +++ b/examples/impress_r/plot_rome_scores.py @@ -10,7 +10,7 @@ python plot_rome_scores.py [--csv-dir DIR] [--log FILE] [--out-dir DIR] Defaults: - --csv-dir /scratch/bblj/$USER/IMPRESS_outputs + --csv-dir /scratch/***/$USER/IMPRESS_outputs --log logs/impress_21736435.out (relative to script dir) --out-dir . (saves next to this script) """ @@ -31,9 +31,12 @@ # ── Argument parsing ────────────────────────────────────────────────────────── parser = argparse.ArgumentParser() -_user = os.environ.get("USER", "mgoliyad1") -_scratch = os.environ.get("SCRATCH", f"/scratch/bblj/{_user}") -parser.add_argument("--csv-dir", default=f"{_scratch}/IMPRESS_outputs") +_scratch = os.environ.get("SCRATCH") +parser.add_argument( + "--csv-dir", + default=f"{_scratch}/IMPRESS_outputs" if _scratch else None, + required=_scratch is None, +) parser.add_argument("--log", default=os.path.join(os.path.dirname(__file__), "logs", "impress_21736435.out")) parser.add_argument("--out-dir", default=os.path.dirname(__file__)) args = parser.parse_args() diff --git a/examples/protein_binding/delta_env_setup.sh b/examples/protein_binding/delta_env_setup.sh index 8caceab..54a0d15 100644 --- a/examples/protein_binding/delta_env_setup.sh +++ b/examples/protein_binding/delta_env_setup.sh @@ -11,12 +11,18 @@ # Defaults: # ENV_DIR = /u/$USER/ve/impress # IMPRESS_DIR = $SCRATCH/$USER/IMPRESS -# python = auto-detected (python/3.11, cray-python/3.11.7, anaconda3) +# python = auto-detected via `module load python` (Delta default: 3.13+) # ============================================================================= if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then set -euo pipefail fi +# ── Initialize lmod (needed when run as non-interactive bash script) ────────── +if ! declare -f module &>/dev/null; then + _lmod_init=/usr/share/lmod/lmod/init/bash + [ -f "${_lmod_init}" ] && source "${_lmod_init}" +fi + # ── Require SCRATCH ─────────────────────────────────────────────────────────── if [[ -z "${SCRATCH:-}" ]]; then echo "ERROR: set the SCRATCH env var to your allocation scratch root, e.g.:" @@ -51,34 +57,22 @@ echo "=================================================================" echo "" echo "── Step 1: Creating venv ──" -_find_python() { - for candidate in python3.12 python3.11 python3 python; do - local p - p=$(command -v "${candidate}" 2>/dev/null) || continue - local ver - ver=$("${p}" -c "import sys; v=sys.version_info; print(v.major*10+v.minor)" 2>/dev/null) || continue - [ "${ver}" -ge 311 ] && echo "${p}" && return 0 - done - return 1 -} - if [ -n "${BASE_PY_OVERRIDE}" ]; then BASE_PY="${BASE_PY_OVERRIDE}" echo "Using Python override: ${BASE_PY}" else - BASE_PY=$(_find_python || true) + # On Delta, `module load python` gives the default Python 3.13+. + module load python 2>/dev/null || true + BASE_PY=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true) if [ -z "${BASE_PY}" ]; then - echo "python3.11+ not in PATH — trying modules..." - for mod in python/3.12 python/3.11 cray-python/3.11.7 anaconda3; do - module load "${mod}" 2>/dev/null || true - BASE_PY=$(_find_python || true) - [ -n "${BASE_PY}" ] && echo " loaded module: ${mod}" && break - done + echo "ERROR: no Python found after 'module load python'." + echo " Pass an explicit interpreter: --python /path/to/python3" + exit 1 fi - if [ -z "${BASE_PY}" ]; then - echo "ERROR: no Python 3.11+ interpreter found." - echo " Pass an explicit interpreter: --python /path/to/python3.11" - echo " Or load a module manually before running this script." + ver=$("${BASE_PY}" -c "import sys; v=sys.version_info; print(v.major*100+v.minor)") + if [ "${ver}" -lt 311 ]; then + echo "ERROR: ${BASE_PY} is Python ${ver} — need 3.11+." + echo " Pass an explicit interpreter: --python /path/to/python3.11" exit 1 fi fi @@ -107,6 +101,10 @@ echo "── Step 3: radical-asyncflow (PyPI) ──" echo "" echo "── Step 4: rhapsody-py[dragon] (PyPI) ──" "${PIP}" install -q "rhapsody-py[dragon,telemetry]" +# Pin dragonhpc to 0.14.1 — 0.14.2 added waitForKeys to DDRegisterClientResponse +# but the Delta system Dragon runtime has not been updated to match; 0.14.2 fails +# with AttributeError on every DDict operation on this cluster. +"${PIP}" install -q "dragonhpc==0.14.1" # ── 5. IMPRESS (local editable) ─────────────────────────────────────────────── echo "" @@ -120,23 +118,27 @@ echo "── Step 6: PyTorch (cu121) ──" # ── 7. Additional dependencies ─────────────────────────────────────────────── echo "" -echo "── Step 7: pandas + biopandas ──" -"${PIP}" install -q pandas biopandas +echo "── Step 7: pandas + biopandas + matplotlib ──" +"${PIP}" install -q pandas biopandas matplotlib -# ── 8. PyRosetta (via pyrosetta-installer) ─────────────────────────────────── +# ── 8. PyRosetta ───────────────────────────────────────────────────────────── echo "" echo "── Step 8: PyRosetta ──" +# pyrosetta_installer handles credential lookup internally. +# Activate the venv in the environment so its subprocess pip installs there. +export VIRTUAL_ENV="${ENV_DIR}" +export PATH="${ENV_DIR}/bin:${PATH}" "${PIP}" install -q pyrosetta-installer "${PY}" -c "import pyrosetta_installer; pyrosetta_installer.install_pyrosetta()" -# ── 9. Boltz (structure prediction — separate Python 3.12 conda env) ───────── +# ── 9. Boltz (structure prediction — separate conda env) ───────────────────── echo "" echo "── Step 9: Boltz (separate conda env) ──" -# boltz 2.x requires numpy<2.0, scipy==1.13.1, etc. — none have Python 3.13 -# wheels, so boltz cannot be installed in the main Python 3.13 IMPRESS venv. -# Create a dedicated Python 3.12 conda env and install boltz there. +# boltz 2.x is kept in a separate conda env so its dependency pins (scipy, etc.) +# don't constrain the main IMPRESS venv. Boltz 2.0+ also installs fine via pip +# in Python 3.13, but we keep the separation to avoid pin conflicts. # s4_boltz.sh activates BOLTZ_VENV (set in delta_gpu_run.sh) instead of VIRTUAL_ENV. -MINIFORGE="${MINIFORGE:-/scratch/bblj/${USER}/miniforge3}" +MINIFORGE="${MINIFORGE:-${SCRATCH}/${USER}/miniforge3}" BOLTZ_ENV="${BOLTZ_ENV:-${HOME}/ve/boltz}" # Cache lives in home dir — scratch inode quota can't hold the 45K CCD files. BOLTZ_CACHE="${BOLTZ_CACHE:-${HOME}/boltz}" @@ -170,7 +172,7 @@ BOLTZ_MSA_CACHE="${BOLTZ_CACHE}/msa_cache" mkdir -p "${BOLTZ_MSA_CACHE}" echo " Pre-computing MSAs into ${BOLTZ_MSA_CACHE}" # IMPRESS_BASE_DIR = parent of prod_in/; IMPRESS_OUTPUT_DIR = parent of af_pipeline_outputs_multi/ -_scratch="${SCRATCH:-/scratch/bblj/${USER}}" +_scratch="${SCRATCH}" _base_dir="${IMPRESS_BASE_DIR:-${_scratch}/IMPRESS_inputs}" _out_dir="${IMPRESS_OUTPUT_DIR:-${_scratch}/IMPRESS_outputs}" _msa_inputs_dir="${_base_dir}/prod_in" diff --git a/examples/protein_binding/delta_gpu_run.sh b/examples/protein_binding/delta_gpu_run.sh index 7d333b0..f07c51e 100644 --- a/examples/protein_binding/delta_gpu_run.sh +++ b/examples/protein_binding/delta_gpu_run.sh @@ -47,7 +47,7 @@ export FAB_LIB=/opt/cray/libfabric/1.22.0/lib64 export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH:-} # ── Environment ─────────────────────────────────────────────────────────────── -IMPRESS_VENV="${IMPRESS_VENV:-${HOME}/ve/impress}" +IMPRESS_VENV="${IMPRESS_VENV:-${HOME}/ve/impress_A}" unset SLURM_EXPORT_ENV source "${IMPRESS_VENV}/bin/activate" dragon-config add --ofi-runtime-lib="${FAB_LIB}" diff --git a/examples/small_molecule_binding/delta_env_setup.sh b/examples/small_molecule_binding/delta_env_setup.sh index 528b888..9f4083e 100755 --- a/examples/small_molecule_binding/delta_env_setup.sh +++ b/examples/small_molecule_binding/delta_env_setup.sh @@ -9,9 +9,9 @@ # bash delta_env_setup.sh [--env-dir DIR] [--impress-dir DIR] [--python PATH] # # Defaults: -# ENV_DIR = /u/$USER/ve/impress +# ENV_DIR = /u/$USER/ve/small_mol # IMPRESS_DIR = $SCRATCH/$USER/IMPRESS -# python = auto-detected (python/3.11, cray-python/3.11.7, anaconda3) +# python = auto-detected via `module load python` (Delta default: 3.13+) # # Tool directories (cloned by this script if absent): # MPNN_DIR = $SCRATCH/$USER/LigandMPNN @@ -26,6 +26,12 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then set -euo pipefail fi +# ── Initialize lmod (needed when run as non-interactive bash script) ────────── +if ! declare -f module &>/dev/null; then + _lmod_init=/usr/share/lmod/lmod/init/bash + [ -f "${_lmod_init}" ] && source "${_lmod_init}" +fi + # ── Require SCRATCH ─────────────────────────────────────────────────────────── if [[ -z "${SCRATCH:-}" ]]; then echo "ERROR: set the SCRATCH env var to your allocation scratch root, e.g.:" @@ -35,7 +41,7 @@ if [[ -z "${SCRATCH:-}" ]]; then fi # ── Defaults / arg parsing ──────────────────────────────────────────────────── -ENV_DIR="${ENV_DIR:-/u/${USER}/ve/impress}" +ENV_DIR="${ENV_DIR:-/u/${USER}/ve/small_mol}" IMPRESS_DIR="${IMPRESS_DIR:-${SCRATCH}/${USER}/IMPRESS}" BASE_PY_OVERRIDE="" @@ -65,34 +71,22 @@ echo "=================================================================" echo "" echo "── Step 1: Creating venv ──" -_find_python() { - for candidate in python3.12 python3.11 python3 python; do - local p - p=$(command -v "${candidate}" 2>/dev/null) || continue - local ver - ver=$("${p}" -c "import sys; v=sys.version_info; print(v.major*10+v.minor)" 2>/dev/null) || continue - [ "${ver}" -ge 311 ] && echo "${p}" && return 0 - done - return 1 -} - if [ -n "${BASE_PY_OVERRIDE}" ]; then BASE_PY="${BASE_PY_OVERRIDE}" echo "Using Python override: ${BASE_PY}" else - BASE_PY=$(_find_python || true) + # On Delta, `module load python` gives the default Python 3.13+. + module load python 2>/dev/null || true + BASE_PY=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true) if [ -z "${BASE_PY}" ]; then - echo "python3.11+ not in PATH — trying modules..." - for mod in python/3.12 python/3.11 cray-python/3.11.7 anaconda3; do - module load "${mod}" 2>/dev/null || true - BASE_PY=$(_find_python || true) - [ -n "${BASE_PY}" ] && echo " loaded module: ${mod}" && break - done + echo "ERROR: no Python found after 'module load python'." + echo " Pass an explicit interpreter: --python /path/to/python3" + exit 1 fi - if [ -z "${BASE_PY}" ]; then - echo "ERROR: no Python 3.11+ interpreter found." - echo " Pass an explicit interpreter: --python /path/to/python3.11" - echo " Or load a module manually before running this script." + ver=$("${BASE_PY}" -c "import sys; v=sys.version_info; print(v.major*100+v.minor)") + if [ "${ver}" -lt 311 ]; then + echo "ERROR: ${BASE_PY} is Python ${ver} — need 3.11+." + echo " Pass an explicit interpreter: --python /path/to/python3.11" exit 1 fi fi @@ -121,6 +115,10 @@ echo "── Step 3: radical-asyncflow (PyPI) ──" echo "" echo "── Step 4: rhapsody-py[dragon] (PyPI) ──" "${PIP}" install -q "rhapsody-py[dragon,telemetry]" +# Pin dragonhpc to 0.14.1 — 0.14.2 added waitForKeys to DDRegisterClientResponse +# but the Delta system Dragon runtime has not been updated to match; 0.14.2 fails +# with AttributeError on every DDict operation on this cluster. +"${PIP}" install -q "dragonhpc==0.14.1" # ── 5. IMPRESS (local editable) ─────────────────────────────────────────────── echo "" @@ -184,12 +182,16 @@ echo "── Step 9: gemmi ──" # ── 10. Additional dependencies ─────────────────────────────────────────────── echo "" -echo "── Step 10: pandas + biopandas ──" -"${PIP}" install -q pandas biopandas +echo "── Step 10: pandas + biopandas + matplotlib ──" +"${PIP}" install -q pandas biopandas matplotlib # ── 11. PyRosetta ───────────────────────────────────────────────────────────── echo "" echo "── Step 11: PyRosetta ──" +# pyrosetta_installer handles credential lookup internally. +# Activate the venv in the environment so its subprocess pip installs there. +export VIRTUAL_ENV="${ENV_DIR}" +export PATH="${ENV_DIR}/bin:${PATH}" "${PIP}" install -q pyrosetta-installer "${PY}" -c "import pyrosetta_installer; pyrosetta_installer.install_pyrosetta()" @@ -203,8 +205,9 @@ echo "── Step 12: ColabFold model weights ──" mkdir -p "${COLABFOLD_CACHE_DIR}" echo " Downloading AlphaFold2 weights to ${COLABFOLD_CACHE_DIR} ..." "${PY}" -c " +from pathlib import Path from colabfold.download import download_alphafold_params -download_alphafold_params('alphafold2', '${COLABFOLD_CACHE_DIR}') +download_alphafold_params('alphafold2', Path('${COLABFOLD_CACHE_DIR}')) print(' Weights downloaded.') " diff --git a/examples/small_molecule_binding/delta_gpu_run.sh b/examples/small_molecule_binding/delta_gpu_run.sh index 9b4852a..c897d21 100644 --- a/examples/small_molecule_binding/delta_gpu_run.sh +++ b/examples/small_molecule_binding/delta_gpu_run.sh @@ -59,7 +59,7 @@ export FAB_LIB=/opt/cray/libfabric/1.22.0/lib64 export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${MPI_LIB}:${FAB_LIB}:${LD_LIBRARY_PATH:-} # ── Environment ─────────────────────────────────────────────────────────────── -IMPRESS_VENV="${IMPRESS_VENV:-${HOME}/ve/impress}" +IMPRESS_VENV="${IMPRESS_VENV:-${HOME}/ve/small_mol}" unset SLURM_EXPORT_ENV source "${IMPRESS_VENV}/bin/activate" dragon-config add --ofi-runtime-lib="${FAB_LIB}" diff --git a/examples/small_molecule_binding/small_molecule_binding.py b/examples/small_molecule_binding/small_molecule_binding.py index d514945..4741c9a 100644 --- a/examples/small_molecule_binding/small_molecule_binding.py +++ b/examples/small_molecule_binding/small_molecule_binding.py @@ -227,7 +227,7 @@ async def rfd3(): output_dir = f"{taskdir}/out" input_pdb = self.state.get('rfd3_input_pdb') - scaffold_arg = f"scaffoldguided.target_pdb={input_pdb}" if input_pdb else "" + scaffold_arg = f"+scaffoldguided.target_pdb={input_pdb}" if input_pdb else "" cmd = ( f"bash {self.scripts_path}/rfd3.sh" From d262a901c62aa21e1dc49cfae120a0b54ab27295 Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Fri, 4 Sep 2026 07:53:33 -0500 Subject: [PATCH 10/12] updated to allow a shorter run to test the flow --- examples/impress_r/delta_gpu_run.sh | 35 ++++++++++++++----- .../impress_r/run_protein_binding_rome.py | 9 +++-- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/examples/impress_r/delta_gpu_run.sh b/examples/impress_r/delta_gpu_run.sh index 7b66ed9..2a263e0 100755 --- a/examples/impress_r/delta_gpu_run.sh +++ b/examples/impress_r/delta_gpu_run.sh @@ -7,19 +7,27 @@ # export SCRATCH=/scratch/ # # Key env vars (all have defaults): -# MPNN_PATH — dauparas/ProteinMPNN checkout (inference AND fine-tune target) -# ROME_MPNN_REPO — same as MPNN_PATH by default -# ROME_TRAINER — mpnn (real fine-tune) | dummy (smoke test, default) -# ROME_MIN_SAMPLES — corpus size before the first training round (default 2) -# ROME_MAX_PASSES — max design passes per pipeline (default 10) -# ROME_FALLBACK — seconds to wait for a training result from Dragon (default 60) -# IMPRESS_BASE_DIR — parent of prod_in/ (input PDB files) -# IMPRESS_OUTPUT_DIR — where af_pipeline_outputs_multi/ is written +# MPNN_PATH — dauparas/ProteinMPNN checkout (inference AND fine-tune target) +# ROME_MPNN_REPO — same as MPNN_PATH by default +# ROME_TRAINER — mpnn (real fine-tune) | dummy (smoke test, default) +# ROME_MIN_SAMPLES — corpus size before the first training round (default 2) +# ROME_MAX_PASSES — max design passes per pipeline (default 10) +# ROME_FALLBACK — seconds Dragon may take to deliver a training result (default 60) +# IMPRESS_N_PIPELINES — number of top-level pipelines to run (default 16) +# IMPRESS_MAX_SUB_PIPELINES — max child pipeline depth per parent (default 3; 0 = none) +# IMPRESS_BASE_DIR — parent of prod_in/ (input PDB files) +# IMPRESS_OUTPUT_DIR — where af_pipeline_outputs_multi/ is written # # Debug flags: # IMPRESS_TEST_MODE=1 — 2 pipelines, max_passes=1, no child pipelines # ROME_TRAINER=dummy — skip real fine-tuning (useful with IMPRESS_TEST_MODE) # +# Quick validation run (~1 h, sees full ROME loop to completion): +# IMPRESS_N_PIPELINES=4 IMPRESS_MAX_SUB_PIPELINES=1 ROME_MAX_PASSES=8 ROME_FALLBACK=120 sbatch delta_gpu_run.sh +# +# Full production run (~3 h): +# sbatch delta_gpu_run.sh +# # Example: # sbatch delta_gpu_run.sh # IMPRESS_TEST_MODE=1 ROME_TRAINER=dummy sbatch delta_gpu_run.sh @@ -87,7 +95,16 @@ export IMPRESS_OUTPUT_DIR="${IMPRESS_OUTPUT_DIR:-${SCRATCH}/${USER}/IMPRESS_outp export ROME_TRAINER="${ROME_TRAINER:-mpnn}" export ROME_MIN_SAMPLES="${ROME_MIN_SAMPLES:-2}" export ROME_MAX_PASSES="${ROME_MAX_PASSES:-10}" -export ROME_FALLBACK="${ROME_FALLBACK:-60}" +# 120 s gives training rounds time to finish and write train_complete before Dragon's +# result-delivery future raises a spurious TypeError (dragonhpc 0.14.1 DDict race). +export ROME_FALLBACK="${ROME_FALLBACK:-120}" + +# ── Scale settings ──────────────────────────────────────────────────────────── +# Quick run (~1 h): IMPRESS_N_PIPELINES=4 IMPRESS_MAX_SUB_PIPELINES=1 ROME_MAX_PASSES=8 +# Full run (~3 h): leave unset (16 pipelines, 3 sub-levels, 10 passes) +export IMPRESS_N_PIPELINES="${IMPRESS_N_PIPELINES:-16}" +# Blank = use code default (3); set to 0 to disable child-pipeline spawning entirely. +export IMPRESS_MAX_SUB_PIPELINES="${IMPRESS_MAX_SUB_PIPELINES:-}" # ── Test / debug flags ──────────────────────────────────────────────────────── export IMPRESS_TEST_MODE="${IMPRESS_TEST_MODE:-0}" diff --git a/examples/impress_r/run_protein_binding_rome.py b/examples/impress_r/run_protein_binding_rome.py index 350ab52..27c8f3f 100644 --- a/examples/impress_r/run_protein_binding_rome.py +++ b/examples/impress_r/run_protein_binding_rome.py @@ -40,9 +40,12 @@ # Runs 2 pipelines with max_passes=1 and no child pipelines, so a single # MPNN → Boltz → pLDDT → ROME cycle completes for integration testing. TEST_MODE = os.getenv("IMPRESS_TEST_MODE", "0") == "1" -N_PIPELINES = 2 if TEST_MODE else 16 -MAX_PASSES = 1 if TEST_MODE else int(os.environ.get("ROME_MAX_PASSES", 10)) -MAX_SUB_PIPELINES_OVERRIDE = 0 if TEST_MODE else None # None = use inline default (3) +N_PIPELINES = 2 if TEST_MODE else int(os.environ.get("IMPRESS_N_PIPELINES", 16)) +MAX_PASSES = 1 if TEST_MODE else int(os.environ.get("ROME_MAX_PASSES", 10)) +# IMPRESS_MAX_SUB_PIPELINES: max depth of child pipeline spawning (0 = none, 3 = full). +# Unset or blank → inherit the inline default of 3. +_sub_env = os.environ.get("IMPRESS_MAX_SUB_PIPELINES", "").strip() +MAX_SUB_PIPELINES_OVERRIDE = 0 if TEST_MODE else (int(_sub_env) if _sub_env else None) # ── ProteinMPNN repo ─────────────────────────────────────────────────────────── From 5f8086d7678ce854f49e0ee929b8f4a25e199242 Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Fri, 4 Sep 2026 08:06:18 -0500 Subject: [PATCH 11/12] fixed unit tests and upgraded python version --- pyproject.toml | 4 ++-- src/impress/gpu.py | 1 + src/impress/impress_manager.py | 4 ++-- src/impress/pipelines/setup.py | 4 ++-- src/impress/utils/logger.py | 9 +++++++-- tests/unit/test_manager_life_cycle.py | 8 ++++---- tests/unit/test_manager_pipeline_submission.py | 6 +++--- tests/unit/test_pipeline_management.py | 2 +- tests/unit/test_pipeline_setup.py | 1 - tox.ini | 4 ++-- 10 files changed, 24 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9daa18b..cef0ce6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ maintainers = [ {name = "Aymen Alsaadi", email = "aymen.alsaadi@rutgers.edu"}, ] readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.11" dependencies = [ "radical.pilot", @@ -51,7 +51,7 @@ doc = [ [tool.ruff] line-length = 88 -target-version = "py39" +target-version = "py311" fix = true [tool.ruff.lint] diff --git a/src/impress/gpu.py b/src/impress/gpu.py index a429de0..3189590 100644 --- a/src/impress/gpu.py +++ b/src/impress/gpu.py @@ -13,6 +13,7 @@ def _find_gpus() -> list: return [int(g) for g in cuda_visible.split(",") if g.strip().isdigit()] try: from dragon.native.machine import System + sys_info = System() return [gpu for node in sys_info.nodes for gpu in node.gpus] except Exception: diff --git a/src/impress/impress_manager.py b/src/impress/impress_manager.py index 9c100f3..b9446f7 100644 --- a/src/impress/impress_manager.py +++ b/src/impress/impress_manager.py @@ -1,8 +1,8 @@ import asyncio import os import tempfile -from collections.abc import Awaitable -from typing import Any, Callable, Optional, Union +from collections.abc import Awaitable, Callable +from typing import Any, Optional, Union from radical.asyncflow import WorkflowEngine diff --git a/src/impress/pipelines/setup.py b/src/impress/pipelines/setup.py index e0b63e0..4e10036 100644 --- a/src/impress/pipelines/setup.py +++ b/src/impress/pipelines/setup.py @@ -1,5 +1,5 @@ -from collections.abc import Awaitable -from typing import Annotated, Any, Callable, Optional +from collections.abc import Awaitable, Callable +from typing import Annotated, Any, Optional from pydantic import BaseModel, Field, field_validator diff --git a/src/impress/utils/logger.py b/src/impress/utils/logger.py index 4212385..2919c8f 100644 --- a/src/impress/utils/logger.py +++ b/src/impress/utils/logger.py @@ -42,8 +42,13 @@ class ImpressLogger: LogLevel.CRITICAL, ] - def __init__(self, name="ImpressManager", use_colors=True, output_stream=None, - min_level: LogLevel = LogLevel.DEBUG): + def __init__( + self, + name="ImpressManager", + use_colors=True, + output_stream=None, + min_level: LogLevel = LogLevel.DEBUG, + ): self.name = name self.use_colors = use_colors self.output_stream = output_stream or sys.stdout diff --git a/tests/unit/test_manager_life_cycle.py b/tests/unit/test_manager_life_cycle.py index 38f7806..bd110b3 100644 --- a/tests/unit/test_manager_life_cycle.py +++ b/tests/unit/test_manager_life_cycle.py @@ -12,7 +12,7 @@ class MockWorkflowEngine: """Mock workflow engine""" @classmethod - async def create(cls, backend=None): + async def create(cls, backend=None, **kwargs): return cls() async def shutdown(self): @@ -126,8 +126,8 @@ class ExplodingPipeline(MockPipeline): async def run(self): raise RuntimeError("pipeline exploded") - await impress_manager.start([ - {"name": "boom", "type": ExplodingPipeline, "config": {}, "kwargs": {}} - ]) + await impress_manager.start( + [{"name": "boom", "type": ExplodingPipeline, "config": {}, "kwargs": {}}] + ) # If shutdown() raises AttributeError, the try/finally fix is broken assert len(impress_manager.pipeline_tasks) == 0 diff --git a/tests/unit/test_manager_pipeline_submission.py b/tests/unit/test_manager_pipeline_submission.py index 50999b4..92bbdfa 100644 --- a/tests/unit/test_manager_pipeline_submission.py +++ b/tests/unit/test_manager_pipeline_submission.py @@ -70,6 +70,6 @@ def test_submit_multiple_pipelines(self, mock_create_task, impress_manager): def test_submit_before_start_raises(self, impress_manager): """submit_new_pipelines raises RuntimeError when called before start()""" with pytest.raises(RuntimeError, match="start\\(\\) must be called"): - impress_manager.submit_new_pipelines([ - {"name": "p", "type": MockPipeline, "config": {}, "kwargs": {}} - ]) + impress_manager.submit_new_pipelines( + [{"name": "p", "type": MockPipeline, "config": {}, "kwargs": {}}] + ) diff --git a/tests/unit/test_pipeline_management.py b/tests/unit/test_pipeline_management.py index af35ae9..593327f 100644 --- a/tests/unit/test_pipeline_management.py +++ b/tests/unit/test_pipeline_management.py @@ -82,7 +82,7 @@ async def run(self): # Run the manager with timeout try: await asyncio.wait_for(impress_manager.start([pipeline_setup]), timeout=3.0) - except asyncio.TimeoutError: + except TimeoutError: # Print debug info if it times out print(f"Completed pipelines: {completed_pipelines}") print(f"Pipeline tasks: {len(impress_manager.pipeline_tasks)}") diff --git a/tests/unit/test_pipeline_setup.py b/tests/unit/test_pipeline_setup.py index 7212f14..c56d398 100644 --- a/tests/unit/test_pipeline_setup.py +++ b/tests/unit/test_pipeline_setup.py @@ -1,4 +1,3 @@ - import pytest from pydantic import ValidationError diff --git a/tox.ini b/tox.ini index 11f11ba..fb0ef4d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39,py310,py311,py312,py313 +envlist = py311,py312,py313 isolated_build = true # Unit test env @@ -8,7 +8,7 @@ extras = dev commands = pytest tests/unit {posargs} # Integration test radical.pilot -[testenv:{py39,py310,py311,py312,py313}-all] +[testenv:{py311,py312,py313}-all] extras = dev setenv = RADICAL_VERBOSE=DEBUG From b42e3a3e123af05bbd55550c2ded82132b1787b8 Mon Sep 17 00:00:00 2001 From: Mariya Goliyad Date: Fri, 4 Sep 2026 08:11:07 -0500 Subject: [PATCH 12/12] fixed unit tests and upgraded python version #2 --- .github/workflows/tests.yml | 12 ----- src/CODE_REVIEW.md | 98 ------------------------------------- 2 files changed, 110 deletions(-) delete mode 100644 src/CODE_REVIEW.md diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f9e6856..de003c5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,12 +14,6 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-latest - python: '3.9' - toxenv: py39 - - os: ubuntu-latest - python: '3.10' - toxenv: py310 - os: ubuntu-latest python: '3.11' toxenv: py311 @@ -59,12 +53,6 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-latest - python: '3.9' - toxenv: py39-all - - os: ubuntu-latest - python: '3.10' - toxenv: py310-all - os: ubuntu-latest python: '3.11' toxenv: py311-all diff --git a/src/CODE_REVIEW.md b/src/CODE_REVIEW.md deleted file mode 100644 index 890b63b..0000000 --- a/src/CODE_REVIEW.md +++ /dev/null @@ -1,98 +0,0 @@ -# IMPRESS `src/` Code Review - -**Date:** 2026-08-29 -**Scope:** `/scratch/bblj/mgoliyad1/IMPRESS/src/impress/` -**Status:** bugs marked ✅ fixed or ⚠️ open -**Note:** `protein_binding.py` was moved from `src/impress/pipelines/` to -`examples/protein_binding/` in the `origin/main` merge; findings for that file -are now in `examples/protein_binding/CODE_REVIEW.md`. - ---- - -## Bugs - -### ✅ `impress_manager.py:164` — `kill_parent` path crashes on unpack - -When a pipeline sets `kill_parent=True`, the manager appended the bare `pipeline` -object to `completed_pipelines`: - -```python -completed_pipelines.append(pipeline) # was wrong -``` - -The cleanup loop unpacks `for pipeline, future in completed_pipelines:`, so this -raises `ValueError: not enough values to unpack` the moment any pipeline is killed. - -**Fix applied:** changed to `completed_pipelines.append((pipeline, pipeline_future))`. - ---- - -## Potential Issues - -### `impress_manager.py` — `WorkflowEngine` leaks on exception - -`start()` creates `self.flow = await WorkflowEngine.create(...)` but never calls -`self.flow.shutdown()`. The caller is responsible for cleanup, but if `start()` -raises mid-run the caller's post-`await` shutdown line is never reached and the -engine leaks. - -**Recommended fix:** wrap the main loop in `try/finally` inside `start()`, or -document that callers must guard with `try/finally`. - ---- - -### `impress_pipeline.py:99` — `finalize` abstract/async mismatch - -`ImpressBasePipeline` declares: - -```python -@abstractmethod -async def finalize(self): - """Optional: Cleanup or finalization logic""" -``` - -Both `SmallMoleculeBindingPipeline` and `ProteinBindingPipeline` implement it as -a plain `def finalize(self, ...)` (synchronous, with extra args). This means: - -- Calling `await pipeline.finalize()` on a subclass instance would fail because - the method is sync. -- The docstring says "Optional" but `@abstractmethod` makes it mandatory. - -**Recommended fix:** either remove `@abstractmethod` and provide a no-op base -implementation, or declare it consistently as sync across the hierarchy. - ---- - -## Code Quality - -### `logger.py` — no log level filtering - -`LogLevel` enum is defined but never used to filter output. Every `debug()` call -always prints regardless of any configured level. The `activity_summary` method is -at DEBUG level but fires on every active cycle, producing steady noise. A minimum -configurable log level check should be added to `_write_log` or each log method. - ---- - -### `logger.py` — `error()` ignores `self.output_stream` - -`error()` and `critical()` bypass `self.output_stream` and always write to -`sys.stderr` directly via `_write_log(formatted, to_stderr=True)`. A caller that -sets a custom output stream (e.g. for testing) will silently lose error messages. - ---- - -### `impress_manager.py:222` — `activity_summary` shows stale buffer count - -`len(self.new_pipeline_buffer)` is logged *after* `self.new_pipeline_buffer.clear()` -runs (line 217), so the buffered count is always reported as 0. Move the summary -log before the clear, or capture the count beforehand. - ---- - -### `impress_manager.py` — `self.flow` only exists after `start()` is called - -`submit_new_pipelines()` is a public method that references `self.flow`, but -`self.flow` is created inside `start()`. Calling `submit_new_pipelines()` directly -before `start()` raises `AttributeError`. Either initialise `self.flow = None` in -`__init__` with a guard, or make `submit_new_pipelines` private.