Commit 2337c1e2 by Cassandra Masschelein Committed by GitHub

Merge pull request #1 from schwallergroup/dev

DynaMate for multiligand systems
parents 52cf521b 1315866d
......@@ -52,7 +52,71 @@ class MDAgent(BaseAgent):
self.completed_steps = []
self.completed_summary = ""
self.EXPECTED_FILES = ["md.tpr", "md.xtc", "md.edr", "md.log", "md.gro"]
self.EXPECTED_FILES_PROTEIN_TEMPLATE = [
"{pdb_id}.pdb",
"{pdb_id}_prepared.pdb",
"{pdb_id}_prepared_capped.pdb",
"{pdb_id}_prepared_capped_his.pdb",
"{pdb_id}.prmtop",
"{pdb_id}.inpcrd",
"{pdb_id}_tleap.pdb",
"topol.top",
"{pdb_id}.gro",
"topol_without_posre.top",
"em.gro",
"nvt.gro",
"nvt.xtc",
"npt.gro",
"npt.xtc",
"temperature.xvg",
"pressure.xvg",
"density.xvg",
"potential.xvg",
"md.gro",
"md.xtc",
"rmsd.xvg",
"rmsd_xtal.xvg",
"rmsf.xvg",
"gyrate.xvg",
"hbnum_prot_wat.xvg",
"hbnum_sidechain.xvg",
]
self.EXPECTED_FILES_TEMPLATE = [
"{pdb_id}.pdb",
"{pdb_id}_prepared.pdb",
"{ligand_name}.pdb",
["{ligand_name}_h.pdb", "{ligand_name}_1_h.pdb"],
"{pdb_id}_prepared_capped.pdb",
"{pdb_id}_prepared_capped_his.pdb",
["{ligand_name}_h.prepi", "{ligand_name}_1_h.prepi"],
"{ligand_name}.frcmod",
"complex.pdb",
"complex.prmtop",
"complex.inpcrd",
"complex_tleap.pdb",
"topol.top",
"complex.gro",
"topol_without_posre.top",
"{ligand_name}.gro",
"em.gro",
"nvt.gro",
"nvt.xtc",
"npt.gro",
"npt.xtc",
"temperature.xvg",
"pressure.xvg",
"density.xvg",
"potential.xvg",
"md.gro",
"md.xtc",
"rmsd.xvg",
"rmsd_xtal.xvg",
"rmsf.xvg",
"gyrate.xvg",
"hbnum_prot_lig.xvg",
"hbnum_prot_wat.xvg",
"hbnum_sidechain.xvg",
]
self.logger.info(f"MDAgent initialized.")
......@@ -60,17 +124,21 @@ class MDAgent(BaseAgent):
if (tool_name in ("gromacs_production", "gromacs_equil", "gromacs_analysis")) and (
" failed with return code " in tool_call
):
self.logger.error(f"{tool_call}")
return False
raise ToolOutputError(f"Gromacs tool execution failed: {tool_call}")
if tool_name in ("run_tleap", "run_tleap_ligand"):
if tool_name in ("run_tleap", "run_tleap_ligand", "param_ligand"):
if "tleap run failed with error:" in tool_call:
self.logger.error(f"{tool_call}")
return False
if "Ligand parameterization failed with error:" in tool_call:
self.logger.error(f"{tool_call}")
return False
raise ToolOutputError(f"TLEaP run failed {tool_call}")
if "ParmEd failed:" in tool_call:
self.logger.error(f"{tool_call}")
return False
raise ToolOutputError(f"ParmEd failed {tool_call}")
return True
......@@ -200,12 +268,56 @@ class MDAgent(BaseAgent):
return list(self.ESSENTIAL_STEPS)
def _format_expected_files(self, templates):
values = {
"pdb_id": self.pdb_id.upper(),
"ligand_name": self.ligand_name.upper(),
}
formatted = []
for item in templates:
if isinstance(item, (list, tuple)):
formatted.append(
[s.format(**values) for s in item]
)
else:
formatted.append(
item.format(**values)
)
return formatted
def _resolve_file(self, f):
if isinstance(f, (list, tuple)):
for candidate in f:
path = self.sandbox_dir / candidate
if path.exists():
return candidate
return f[0]
return f
def _pipeline_successful(self) -> bool:
"""Check whether the full MD pipeline completed successfully."""
missing = [f for f in self.EXPECTED_FILES if not (self.sandbox_dir / f).exists()]
expected_files = (
self._format_expected_files(self.EXPECTED_FILES_TEMPLATE)
if self.ligand_name
else self._format_expected_files(self.EXPECTED_FILES_PROTEIN_TEMPLATE)
)
if missing:
self.logger.error(f"Pipeline incomplete: missing final outputs {missing}")
missing_or_empty = []
for f in expected_files:
resolved = self._resolve_file(f)
path = self.sandbox_dir / resolved
if not path.exists() or path.stat().st_size == 0:
missing_or_empty.append(resolved)
if missing_or_empty:
self.logger.error(
f"Pipeline incomplete: missing final outputs {missing_or_empty}"
)
return False
return True
......
......@@ -70,9 +70,11 @@ class PrepAgent(BaseAgent):
def _get_pdb_file_path(self, prompt):
pdb_file_path = None
num_calls = 0
while pdb_file_path is None:
while pdb_file_path is None and num_calls < 5:
response = self._prompt_llm(prompt)
num_calls += 1
self.logger.info(f"Response: {response}")
tool_calls = response.tool_calls
......
......@@ -90,12 +90,33 @@ EOF
if grep -E "system1 +2" topol.top; then #special case for two identical chains named system1
echo "You have two identical chains named system1, therefore only one position restraint file for the first chain will be created." >> $LOG_FILE 2>&1
echo "Creating group for residues ${ranges[0]}" >> $LOG_FILE 2>&1
echo -e "ri ${ranges[0]}\n2 & \"r_${ranges[0]}\"\nq" | $GMX make_ndx -f em.gro -n index.ndx -o index.ndx >> $LOG_FILE 2>&1
if grep -Fq "[ r_${ranges[0]} ]" index.ndx; then # -F for fixed string (so can use [] without putting "^\[ r_${ranges[0]} \]", -q for quiet)
echo "Group r_${ranges[0]} already exists in index.ndx" >> $LOG_FILE 2>&1
else
echo "Adding group r_${ranges[0]} to index.ndx" >> $LOG_FILE 2>&1
echo -e "ri ${ranges[0]}\nq" | $GMX make_ndx -f em.gro -n index.ndx -o index.ndx >> $LOG_FILE 2>&1
fi
if grep -Fq "[ Protein-H_&_r_${ranges[0]} ]" index.ndx; then
echo "Group Protein-H_&_r_${ranges[0]} already exists in index.ndx" >> $LOG_FILE 2>&1
else
echo "Adding group Protein-H_&_r_${ranges[0]} to index.ndx" >> $LOG_FILE 2>&1
echo -e "2 & \"r_${ranges[0]}\"\nq" | $GMX make_ndx -f em.gro -n index.ndx -o index.ndx >> $LOG_FILE 2>&1
fi
else
i=1
for range in "${ranges[@]}"; do
echo "Creating group for residues $range..." >> $LOG_FILE 2>&1
echo -e "ri $range\n2 & \"r_${range}\"\nq" | $GMX make_ndx -f em.gro -n index.ndx -o index.ndx >> $LOG_FILE 2>&1
if grep -Fq "[ r_$range ]" index.ndx; then
echo "Group r_$range already exists in index.ndx" >> $LOG_FILE 2>&1
else
echo "Adding group r_$range to index.ndx" >> $LOG_FILE 2>&1
echo -e "ri $range\nq" | $GMX make_ndx -f em.gro -n index.ndx -o index.ndx >> $LOG_FILE 2>&1
fi
if grep -Fq "[ Protein-H_&_r_$range ]" index.ndx; then
echo "Group Protein-H_&_r_$range already exists in index.ndx" >> $LOG_FILE 2>&1
else
echo "Adding group Protein-H_&_r_$range to index.ndx" >> $LOG_FILE 2>&1
echo -e "2 & \"r_$range\"\nq" | $GMX make_ndx -f em.gro -n index.ndx -o index.ndx >> $LOG_FILE 2>&1
fi
((i++))
done
fi
......@@ -103,7 +124,10 @@ fi
# Step 6: generate posre.itp for each chain
if grep -E "system1 +2" topol.top; then #special case for two identical chains named system1
group_name="Protein-H_&_r_${ranges[0]}"
if ! ls posre.itp 1> /dev/null 2>&1;then
echo "Generating position restraints for chain1" >> $LOG_FILE 2>&1
echo "$group_name" | $GMX genrestr -f em.gro -n index.ndx -o "posre.itp" -fc 1000 1000 1000 >> $LOG_FILE 2>&1
fi
else
i=1
for range in "${ranges[@]}"; do
......
......@@ -126,9 +126,15 @@ def param_ligand(sandbox_dir: str, ligand_files: str | list[str], ligand_name: s
logger.info(f"Mol2 file for ligand {ligand_stem} created")
cmd = shlex.split(f"sed -i 's/UNL/{ligand_name}/g' {sandbox_dir}/{ligand_stem}.mol2")
run_3 = subprocess.run(cmd, cwd=sandbox_dir, capture_output=True, text=True)
if run_3.returncode != 0:
error_text = "\n".join(filter(None, [run_3.stderr, run_3.stdout]))
run_3_UNL = subprocess.run(cmd, cwd=sandbox_dir, capture_output=True, text=True)
if run_3_UNL.returncode != 0:
error_text = "\n".join(filter(None, [run_3_UNL.stderr, run_3_UNL.stdout]))
return f"Ligand parameterization failed with error: {error_text}"
cmd = shlex.split(f"sed -i 's/UNK/{ligand_name}/g' {sandbox_dir}/{ligand_stem}.mol2")
run_3_UNK = subprocess.run(cmd, cwd=sandbox_dir, capture_output=True, text=True)
if run_3_UNK.returncode != 0:
error_text = "\n".join(filter(None, [run_3_UNK.stderr, run_3_UNK.stdout]))
return f"Ligand parameterization failed with error: {error_text}"
# Create prepi file using antechamber
......@@ -143,7 +149,7 @@ def param_ligand(sandbox_dir: str, ligand_files: str | list[str], ligand_name: s
# Create frcmod file using parmchk2
cmd = shlex.split(f"parmchk2 -i {sandbox_dir}/{ligand_stem}.mol2 -f mol2 -o {sandbox_dir}/{ligand_stem}.frcmod")
run_5 = subprocess.run(cmd, cwd=sandbox_dir, capture_output=True, text=True, check=True)
run_5 = subprocess.run(cmd, cwd=sandbox_dir, capture_output=True, text=True)
if run_5.returncode != 0:
error_text = "\n".join(filter(None, [run_5.stderr, run_5.stdout]))
return f"Ligand parameterization failed with error: {error_text}"
......
......@@ -151,14 +151,14 @@ def prepare_pdb_file_ligand(sandbox_dir: str, pdb_id: str, ligand_name: str = No
# Extract ligand
if (ligand_name is not None) and (ligand_name != "XXX") and (ligand_name != "None") and (ligand_name != "None_h"):
# Count number of ligands
resnums = set()
ligand_keys = set()
with open(f"{sandbox_dir}/{pdb_id}.pdb") as f:
for line in f:
if line.startswith("HETATM") and ligand_name in line:
if line.startswith("HETATM") and line[17:20].strip() == ligand_name:
chain = line[21]
resnum = int(line[22:26])
resnums.add(resnum)
print("resnums is: ", resnums)
num_ligands = len(resnums)
ligand_keys.add((chain, resnum))
num_ligands = len(ligand_keys)
logger.info(f"IMPORTANT: Number of ligands {ligand_name} found: {num_ligands}")
if num_ligands == 0:
......@@ -183,12 +183,13 @@ def prepare_pdb_file_ligand(sandbox_dir: str, pdb_id: str, ligand_name: str = No
# Collect HETATM lines by residue number
with open(f"{sandbox_dir}/{pdb_id}.pdb", "r") as infile:
for line in infile:
if line.startswith("HETATM") and ligand_name in line:
resnum = int(line[22:26]) # residue number column
ligands[resnum].append(line)
if line.startswith("HETATM") and line[17:20].strip() == ligand_name:
chain = line[21].strip() or "_"
resnum = int(line[22:26])
ligands[(chain, resnum)].append(line)
# Write one file per ligand
for i, (resnum, atom_lines) in enumerate(ligands.items(), start=1):
for i, ((chain, resnum), atom_lines) in enumerate(ligands.items(), start=1):
ligand_pdb_file = f"{sandbox_dir}/{ligand_name}_{i}.pdb"
ligand_pdb_files_list.append(ligand_pdb_file)
with open(ligand_pdb_file, "w") as outfile:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment