Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# Ignore test data
abfe/tests/test_data/
# Ignore local copy of MDRestraintsGenerator
src/MDRestraintsGenerator/


abfe/tests/test_data/abfe_analysis_input
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
# ABFE_Package
Code for setting up and running replica ABFE simulations
Got it — here’s a clean README section with just your **install from source** steps:

---

## Install from Source

Expand All @@ -23,6 +19,7 @@ conda activate abflow
3. **Install `MDRestraintsGenerator`**

```bash
mkdir src
cd src
git clone https://github.com/Nithishwer/MDRestraintsGenerator.git
cd MDRestraintsGenerator
Expand All @@ -32,7 +29,7 @@ pip install --no-deps .
4. **Install this repository**

```bash
cd ..
cd ../../
pip install .
```

Expand All @@ -50,7 +47,6 @@ MDRestraintsGenerator package needs to be installed with no-dependencies flag to
cd ~/Documents/abflow/src # or wherever you keep your editable sources
git clone https://github.com/Nithishwer/MDRestraintsGenerator.git
cd MDRestraintsGenerator
git checkout 03914398839e9cf912b6da2a4e04ce29b09b69c0
pip install --no-deps -e .
```

Expand Down Expand Up @@ -115,7 +111,7 @@ from abfe.utils.ligand_only_setup import LigandOnlySetup
from pathlib import Path
import logging

# --- 🔧 Define base path and ligand names (EDIT THIS ONLY) ---
# --- Define base path and ligand names (EDIT THIS ONLY) ---
base_path = "/ABS/PATH/TO/WORKDIR"
ligands = ["Ligand1", "Ligand2"] # Each should be a subfolder under base_path

Expand Down Expand Up @@ -170,7 +166,7 @@ The `ABFEAnalyzer` class performs automated analysis of complex-leg and ligand-o
```python
from abfe.analyse_abfe import ABFEAnalyzer

# --- 🔧 Define your system path and ligands ---
# --- Define your system path and ligands ---
base_path = "/ABS/PATH/TO/WORKDIR" # Folder containing all ligand folders
ligand_folders = ["Ligand1", "Ligand2"] # Replace with your actual ligand folder names
n_replicates = 3 # Number of ABFE replicates per ligand
Expand Down
Binary file added abfe/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file modified abfe/__pycache__/analyse_abfe.cpython-310.pyc
Binary file not shown.
Binary file not shown.
11 changes: 6 additions & 5 deletions abfe/analyse_abfe.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(
self.abfe_replicates = abfe_replicates
self.ignore_folders = ignore_folders or []
self.legs_config = legs_config or {
"complex": {"bonded": 11, "coul": 10, "vdw": 21},
"complex": {"rest": 11, "coul": 10, "vdw": 21},
"ligand": {"coul": 11, "vdw": 21},
}
self.temperature = temperature
Expand Down Expand Up @@ -100,14 +100,15 @@ def get_rest_dG(filepath: str) -> float:

@staticmethod
def extract_dG_from_summary(filepath: str, leg: str, stage: str) -> List[float]:
df = pd.read_csv(filepath)
df.columns = ["PROCESS", "ID", "MBAR", "MBAR_Error", "BAR", "BAR_Error", "TI", "TI_Error"]
# Skip the first row to avoid the (0,0,1) orientation tuple
df = pd.read_csv(filepath, skiprows=1)
df.columns = ["PROCESS", "ID", "MBAR", "MBAR_Error",
"BAR", "BAR_Error", "TI", "TI_Error"]
matching = df[df["ID"] == leg]

if matching.empty:
raise ValueError(f"No matching ID='{leg}' found in {filepath}")
if len(matching) > 1:
raise ValueError(f"Multiple rows found for ID='{leg}' in {filepath}. Please ensure unique IDs.")
raise ValueError(f"Multiple rows found for ID='{leg}' in {filepath}.")

row = matching.squeeze()
return [f"{stage}_{leg}"] + row.iloc[2:].astype(float).tolist()
Expand Down
Binary file added abfe/tests/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file modified abfe/tests/__pycache__/datafiles.cpython-310.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
14 changes: 14 additions & 0 deletions abfe/tests/datafiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@

from importlib.resources import files

# Base location for the vanilla input data used by test_setup_vanilla.py
BASE_DATA = files("abfe.tests.test_data.vanilla_input.system_setup")

CHARMM = BASE_DATA / "charmm" / "charmm_gmx"
PROTEIN = BASE_DATA / "protein_prep" / "protein_param"
SDF_DIR = BASE_DATA / "ligands_aligned2charmm"
CRYSTAL_WATER = BASE_DATA / "protein_prep" / "crystal_water_param" / "crystal_waters_aligned2charmm.gro"

# -------------------------------------------------------------------------
# New section for ABFE input data

ABFE_DEFLORIAN = files("abfe.tests.test_data.abfe_input.deflorian")
# Example usage: (ABFE_DEFLORIAN / "A2A_4g").resolve() gives the A2A_4g input



# -------------------------------------------------------------------------
# ABFE analysis input data for test_analysis_abfe.py

ABFE_ANALYSIS_INPUT = files("abfe.tests.test_data.abfe_analysis_input.deflorian")
52 changes: 52 additions & 0 deletions abfe/tests/test_01_scaffold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# abfe/tests/test_01_scaffold.py
import shutil
from pathlib import Path
import pytest

from abfe.setup_vanilla_simulations import SimulationSetup
from abfe.tests import datafiles


def test_scaffold_creates_expected_layout(tmp_path, monkeypatch):
"""Fast: only directory + initial file copies (no heavy setup)."""

# Arrange: temp working dirs and inputs
base_path = tmp_path / "test_setup"
charmm_folder = base_path / "input_charmm"
protein_folder = base_path / "input_protein"
sdf_folder = base_path / "input_ligands"
crystal_water = base_path / "crystal_waters.gro"

shutil.copytree(datafiles.CHARMM.resolve(), charmm_folder)
shutil.copytree(datafiles.PROTEIN.resolve(), protein_folder)
shutil.copytree(datafiles.SDF_DIR.resolve(), sdf_folder)
shutil.copy(datafiles.CRYSTAL_WATER.resolve(), crystal_water)

# Stub out the heavy runner to a no-op so we verify only the scaffold work
monkeypatch.setattr(
SimulationSetup,
"run_vanilla_setup",
lambda self, vp, cn: None,
)

setup = SimulationSetup(
base_path=base_path,
charmm_folder=charmm_folder,
protein_folder=protein_folder,
sdf_folder=sdf_folder,
crystal_water_gro=crystal_water,
solvate_water_count=10286,
crystal_water_count=136,
num_nodes=2,
)

# Act
setup.setup_all(repeats=1)

# Assert
ligand = next(p.stem for p in sdf_folder.glob("*.sdf"))
outdir = base_path / f"complex_{ligand}" / "vanilla_rep_1"
assert outdir.exists(), "vanilla_rep_1 not created."

assert (outdir / f"{ligand}.sdf").exists(), "Ligand SDF not copied."
assert (outdir / "charmm_gmx" / "step5_input.pdb").exists(), "CHARMM dir not copied as charmm_gmx."
29 changes: 29 additions & 0 deletions abfe/tests/test_02_membrane_extract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# abfe/tests/test_02_membrane_extract.py
import shutil
from pathlib import Path

from abfe.tests import datafiles
from abfe.utils.gromacs_helpers_single_chain import extract_membrane_from_charmm


def test_extract_membrane_from_charmm_creates_membrane_gro(tmp_path):
"""Fast: run the real extractor on provided CHARMM box."""
# Arrange
charmm_src = datafiles.CHARMM.resolve()
work = tmp_path / "work"
work.mkdir()
shutil.copytree(charmm_src, work / "charmm_gmx")

charmm_gro = work / "charmm_gmx" / "step5_input.gro"
membrane_out = work / "membrane.gro"

# Act
extract_membrane_from_charmm(str(charmm_gro), str(membrane_out))

# Assert
assert membrane_out.exists(), "membrane.gro not created"
# lightweight content sanity check
text = membrane_out.read_text(errors="ignore")
assert len(text) > 0, "membrane.gro is empty"
# Optional: look for lipid residue hints (POPC etc.) if present in your test data
# assert "POPC" in text or "LIPID" in text
128 changes: 128 additions & 0 deletions abfe/tests/test_03_plumbing_until_solvate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# abfe/tests/test_03_plumbing_until_solvate.py
import shutil
from pathlib import Path

from abfe.setup_vanilla_simulations import SimulationSetup
from abfe.tests import datafiles


def test_run_setup_through_solvation_fast(tmp_path, monkeypatch):
"""
Medium-fast: run run_vanilla_setup with light stubs so we pass:
param_lig -> copy protein -> organize/add ligand files -> extract membrane ->
copy_complex2membrane -> addmemtop -> solvate_system -> cp_mdps
"""

# --- Arrange inputs
base_path = tmp_path / "test_setup"
charmm_folder = base_path / "input_charmm"
protein_folder = base_path / "input_protein"
sdf_folder = base_path / "input_ligands"
crystal_water = base_path / "crystal_waters.gro"

shutil.copytree(datafiles.CHARMM.resolve(), charmm_folder)
shutil.copytree(datafiles.PROTEIN.resolve(), protein_folder)
shutil.copytree(datafiles.SDF_DIR.resolve(), sdf_folder)
shutil.copy(datafiles.CRYSTAL_WATER.resolve(), crystal_water)

# Ensure there's at least one SDF so setup_all() always iterates.
if not any(sdf_folder.glob("*.sdf")):
(sdf_folder / "DUMMY.sdf").write_text("\n")

# --- Patch functions as imported INTO the target module
target_mod = "abfe.setup_vanilla_simulations"

# No-op parametrization (avoid openff/bespokefit)
monkeypatch.setattr(f"{target_mod}.param_lig", lambda sdf: Path("smirnoff").mkdir(exist_ok=True))

# Provide a minimal topology so downstream steps that read/append to topol.top succeed
monkeypatch.setattr(
SimulationSetup,
"copy_parameterized_protein",
lambda self: Path("topol.top").write_text(
"; minimal topol for test\n[ system ]\nTest\n\n[ molecules ]\n; name number\n"
),
)

# Lightweight ligand topology steps
monkeypatch.setattr(f"{target_mod}.organize_topologies", lambda: None)
monkeypatch.setattr(f"{target_mod}.addligtop", lambda sdf: None)
monkeypatch.setattr(f"{target_mod}.addliggro", lambda sdf: None)
monkeypatch.setattr(f"{target_mod}.addligposres", lambda sdf: None)

# FAKE the membrane extraction here to avoid MDAnalysis StopIteration on some fixtures
def _fake_extract_membrane_from_charmm(charmm_gro, membrane_out):
Path(membrane_out).write_text("membrane\n")
monkeypatch.setattr(f"{target_mod}.extract_membrane_from_charmm", _fake_extract_membrane_from_charmm)

# Fake the combine step
def _fake_copy_complex2membrane():
Path("complex_membrane.gro").write_text("combined\n")
monkeypatch.setattr(f"{target_mod}.copy_complex2membrane", _fake_copy_complex2membrane)

# addmemtop can be a no-op
monkeypatch.setattr(f"{target_mod}.addmemtop", lambda sdf: None)

# Fake solvation: create solv.gro and solv_fix.gro
def _fake_solvate(waters: int):
Path("solv.gro").write_text("solv\n")
Path("solv_fix.gro").write_text("solv_fix\n")
monkeypatch.setattr(f"{target_mod}.solvate_system", _fake_solvate)

# Fake mdp copy so a file exists
def _fake_cp_mdps(_):
(Path.cwd() / "mdps").mkdir(exist_ok=True)
(Path("mdps") / "minim.mdp").write_text("; mdp")
monkeypatch.setattr(f"{target_mod}.cp_mdps", _fake_cp_mdps)

monkeypatch.setattr(
f"{target_mod}.merge_gro_files",
lambda file1, file2: Path("solv_fix_crystal_water.gro").write_text("merged\n"),
)
monkeypatch.setattr(f"{target_mod}.update_topology_file", lambda *_: None)
monkeypatch.setattr(f"{target_mod}.add_water2topology", lambda *_: None)

# pretend the ions step succeeded and produced the expected output file
monkeypatch.setattr(
f"{target_mod}.addions",
lambda *_: Path("system_solv_ions.gro").write_text("ions\n"),
)

# pretend index creation succeeded
monkeypatch.setattr(
f"{target_mod}.create_index",
lambda *_: Path("index.ndx").write_text("[ System ]\n1-10\n"),
)

# (optional but safe) avoid touching real cluster templates
monkeypatch.setattr(
f"{target_mod}.copy_jobscripts_vanilla",
lambda templates, name, nodes: Path("submit.sh").write_text("#!/bin/bash\ntrue\n"),
)

setup = SimulationSetup(
base_path=base_path,
charmm_folder=charmm_folder,
protein_folder=protein_folder,
sdf_folder=sdf_folder,
crystal_water_gro=crystal_water,
solvate_water_count=10286,
crystal_water_count=136,
num_nodes=1,
)

# --- Act
setup.setup_all(repeats=1)

# --- Assert
complex_dirs = [p for p in base_path.iterdir() if p.is_dir() and p.name.startswith("complex_")]
assert complex_dirs, "No complex_* output folder was created."
complex_root = complex_dirs[0]
outdir = complex_root / "vanilla_rep_1"

assert outdir.exists(), f"Output folder not created: {outdir}"
assert (outdir / "membrane.gro").exists(), "membrane.gro missing"
assert (outdir / "complex_membrane.gro").exists(), "combined complex/membrane GRO missing"
assert (outdir / "solv_fix.gro").exists(), "solv_fix.gro missing"
assert (outdir / "mdps" / "minim.mdp").exists(), "MDP not copied"
assert (outdir / "topol.top").exists(), "topol.top missing"
Loading