Skip to content

Commit 8674870

Browse files
feat: split BrownDye APBS preparation workflow
Separate AmberTools, APBS, and BrownDye stages so the example produces per-body maps and explicit verification artifacts before simulation. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 21d53fb commit 8674870

7 files changed

Lines changed: 833 additions & 106 deletions

File tree

examples/browndye/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# BrownDye2 Protein-Ligand Example
2+
3+
This example prepares a docked protein-ligand pose for APBS and BrownDye2.
4+
BrownDye treats each moving body as a rigid core, so the workflow generates
5+
separate PQR, APBS DX, and BrownDye XML files for the protein and ligand.
6+
7+
## Workflow
8+
9+
```bash
10+
conda activate ambertools
11+
cd examples/browndye
12+
```
13+
14+
1. Run `complex_pqr.ipynb`.
15+
This writes `tmp/protein_fixed.pdb`, `tmp/ligand.sdf`,
16+
`tmp/ligand_charge.txt`, and `tmp/ligand_resname.txt`.
17+
18+
1. Parameterize with AmberTools:
19+
20+
```bash
21+
bash run_ambertools.sh
22+
```
23+
24+
This writes `tmp/protein.pqr`, `tmp/ligand.pqr`, and `tmp/complex.pqr`.
25+
26+
1. Run APBS:
27+
28+
```bash
29+
bash run_apbs.sh
30+
```
31+
32+
This writes `tmp/protein.dx` and `tmp/ligand.dx`.
33+
34+
1. Build BrownDye inputs:
35+
36+
```bash
37+
bash bdprep.sh
38+
```
39+
40+
By default the script uses `/apps/browndye2`; override with `BD_HOME` or
41+
`BD_BIN` if your BrownDye executables are elsewhere.
42+
43+
1. Run BrownDye and compute a rate estimate:
44+
45+
```bash
46+
bash bdrun.sh
47+
```
48+
49+
For weighted-ensemble mode:
50+
51+
```bash
52+
MODE=we bash bdrun.sh
53+
```
54+
55+
## Important Parameters
56+
57+
- `RXN_SEARCH_DISTANCE`: distance used to find bound-pose contact pairs.
58+
- `RXN_DISTANCE`: BrownDye reaction distance for each selected pair.
59+
- `RXN_NEEDED`: number of contact pairs required for association.
60+
- `N_TRAJECTORIES`: number of BrownDye trajectories.
61+
- `DEBYE_LENGTH`: normally inferred from APBS logs; set explicitly if needed.
62+
- `PQR_STEMS`: APBS input stems, default `protein ligand`.
63+
64+
The default reaction criteria are broad, generated from heavy-atom contacts in
65+
the docked pose. Treat them as a starting point and tune them against structural
66+
or experimental knowledge before interpreting association rates.

examples/browndye/bdprep.sh

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#!/usr/bin/env bash
2+
# bdprep.sh - build BrownDye XML inputs from prepared PQR/APBS files.
3+
#
4+
# Prerequisites:
5+
# conda activate ambertools
6+
# cd examples/browndye
7+
# bash run_ambertools.sh
8+
# bash run_apbs.sh
9+
#
10+
# This script creates:
11+
# protein_atoms.xml, ligand_atoms.xml
12+
# contact_types.xml, reaction_pairs.xml, reactions.xml
13+
# input.xml
14+
# protein_ligand_simulation.xml (via bd_top)
15+
16+
set -euo pipefail
17+
18+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19+
WORKDIR="${WORKDIR:-$SCRIPT_DIR/tmp}"
20+
BD_HOME="${BD_HOME:-/apps/browndye2}"
21+
BD_BIN="${BD_BIN:-$BD_HOME/bin}"
22+
BD_AUX="${BD_AUX:-$BD_HOME/aux}"
23+
24+
CORE0="${CORE0:-protein}"
25+
CORE1="${CORE1:-ligand}"
26+
CORE0_IS_PROTEIN="${CORE0_IS_PROTEIN:-true}"
27+
CORE1_IS_PROTEIN="${CORE1_IS_PROTEIN:-false}"
28+
CORE0_DIELECTRIC="${CORE0_DIELECTRIC:-4.0}"
29+
CORE1_DIELECTRIC="${CORE1_DIELECTRIC:-4.0}"
30+
31+
RXN_SEARCH_DISTANCE="${RXN_SEARCH_DISTANCE:-5.5}"
32+
RXN_DISTANCE="${RXN_DISTANCE:-5.5}"
33+
RXN_NEEDED="${RXN_NEEDED:-3}"
34+
35+
N_THREADS="${N_THREADS:-1}"
36+
SEED="${SEED:-11111111}"
37+
N_TRAJECTORIES="${N_TRAJECTORIES:-1000}"
38+
N_TRAJECTORIES_PER_OUTPUT="${N_TRAJECTORIES_PER_OUTPUT:-10}"
39+
MAX_N_STEPS="${MAX_N_STEPS:-1000000}"
40+
RESULTS_FILE="${RESULTS_FILE:-results.xml}"
41+
42+
SOLVENT_DIELECTRIC="${SOLVENT_DIELECTRIC:-78.0}"
43+
RELATIVE_VISCOSITY="${RELATIVE_VISCOSITY:-1.0}"
44+
KT="${KT:-1.0}"
45+
DESOLVATION_PARAMETER="${DESOLVATION_PARAMETER:-1.0}"
46+
SOLVENT_RADIUS="${SOLVENT_RADIUS:-1.4}"
47+
DEBYE_LENGTH="${DEBYE_LENGTH:-}"
48+
49+
require_file() {
50+
local path="$1"
51+
if [[ ! -s "$path" ]]; then
52+
printf 'Missing required file: %s\n' "$path" >&2
53+
exit 1
54+
fi
55+
}
56+
57+
resolve_tool() {
58+
local tool="$1"
59+
if [[ -x "$BD_BIN/$tool" ]]; then
60+
printf '%s\n' "$BD_BIN/$tool"
61+
return
62+
fi
63+
if [[ -x "$BD_AUX/$tool" ]]; then
64+
printf '%s\n' "$BD_AUX/$tool"
65+
return
66+
fi
67+
if command -v "$tool" >/dev/null 2>&1; then
68+
command -v "$tool"
69+
return
70+
fi
71+
72+
printf 'Missing BrownDye tool: %s\n' "$tool" >&2
73+
printf 'Checked BD_BIN=%s, BD_AUX=%s, and PATH.\n' "$BD_BIN" "$BD_AUX" >&2
74+
printf 'If /apps/browndye2 was built from source, compile/install the aux tools first.\n' >&2
75+
exit 1
76+
}
77+
78+
infer_debye_length() {
79+
python3 - "$CORE0.apbs.log" "$CORE1.apbs.log" <<'PY'
80+
import re
81+
import sys
82+
from pathlib import Path
83+
84+
for name in sys.argv[1:]:
85+
path = Path(name)
86+
if not path.is_file():
87+
continue
88+
text = path.read_text(errors="ignore")
89+
match = re.search(r"[Dd]ebye[- ]length[:\s]+([0-9.]+)", text)
90+
match = match or re.search(r"[Gg]ot debye length\s+([0-9.]+)", text)
91+
if match:
92+
print(match.group(1))
93+
raise SystemExit
94+
raise SystemExit(1)
95+
PY
96+
}
97+
98+
write_contact_types() {
99+
python3 - "$CORE0.pqr" "$CORE1.pqr" >contact_types.xml <<'PY'
100+
import sys
101+
from pathlib import Path
102+
103+
104+
def is_heavy_atom(atom_name: str) -> bool:
105+
stripped = atom_name.strip()
106+
return bool(stripped) and not stripped.upper().startswith("H")
107+
108+
109+
def contacts(path: Path) -> list[tuple[str, str]]:
110+
seen: set[tuple[str, str]] = set()
111+
ordered: list[tuple[str, str]] = []
112+
for line in path.read_text().splitlines():
113+
if not line.startswith(("ATOM", "HETATM")):
114+
continue
115+
parts = line.split()
116+
if len(parts) < 4:
117+
continue
118+
atom = parts[2]
119+
residue = parts[3]
120+
key = (atom, residue)
121+
if is_heavy_atom(atom) and key not in seen:
122+
seen.add(key)
123+
ordered.append(key)
124+
return ordered
125+
126+
127+
mol0 = contacts(Path(sys.argv[1]))
128+
mol1 = contacts(Path(sys.argv[2]))
129+
130+
print("<contacts>")
131+
print(" <combinations>")
132+
for label, entries in (("molecule0", mol0), ("molecule1", mol1)):
133+
print(f" <{label}>")
134+
for atom, residue in entries:
135+
print(f" <contact> <atom> {atom} </atom> <residue> {residue} </residue> </contact>")
136+
print(f" </{label}>")
137+
print(" </combinations>")
138+
print("</contacts>")
139+
PY
140+
}
141+
142+
write_input_xml() {
143+
cat >input.xml <<EOF
144+
<top>
145+
<n_threads> $N_THREADS </n_threads>
146+
<seed> $SEED </seed>
147+
<output> $RESULTS_FILE </output>
148+
149+
<n_trajectories> $N_TRAJECTORIES </n_trajectories>
150+
<n_trajectories_per_output> $N_TRAJECTORIES_PER_OUTPUT </n_trajectories_per_output>
151+
<max_n_steps> $MAX_N_STEPS </max_n_steps>
152+
153+
<system>
154+
<reaction_file> reactions.xml </reaction_file>
155+
156+
<solvent>
157+
<debye_length> $DEBYE_LENGTH </debye_length>
158+
<dielectric> $SOLVENT_DIELECTRIC </dielectric>
159+
<relative_viscosity> $RELATIVE_VISCOSITY </relative_viscosity>
160+
<kT> $KT </kT>
161+
<desolvation_parameter> $DESOLVATION_PARAMETER </desolvation_parameter>
162+
<solvent_radius> $SOLVENT_RADIUS </solvent_radius>
163+
</solvent>
164+
165+
<time_step_tolerances>
166+
<minimum_core_dt> 0.0 </minimum_core_dt>
167+
<minimum_core_reaction_dt> 0.0 </minimum_core_reaction_dt>
168+
</time_step_tolerances>
169+
170+
<group>
171+
<name> $CORE0 </name>
172+
<core>
173+
<name> $CORE0 </name>
174+
<all_in_surface> false </all_in_surface>
175+
<is_protein> $CORE0_IS_PROTEIN </is_protein>
176+
<atoms> ${CORE0}_atoms.xml </atoms>
177+
<electric_field>
178+
<grid> $CORE0.dx </grid>
179+
</electric_field>
180+
<dielectric> $CORE0_DIELECTRIC </dielectric>
181+
</core>
182+
</group>
183+
184+
<group>
185+
<name> $CORE1 </name>
186+
<core>
187+
<name> $CORE1 </name>
188+
<all_in_surface> false </all_in_surface>
189+
<is_protein> $CORE1_IS_PROTEIN </is_protein>
190+
<atoms> ${CORE1}_atoms.xml </atoms>
191+
<electric_field>
192+
<grid> $CORE1.dx </grid>
193+
</electric_field>
194+
<dielectric> $CORE1_DIELECTRIC </dielectric>
195+
</core>
196+
</group>
197+
</system>
198+
</top>
199+
EOF
200+
}
201+
202+
require_file "$WORKDIR/$CORE0.pqr"
203+
require_file "$WORKDIR/$CORE1.pqr"
204+
require_file "$WORKDIR/$CORE0.dx"
205+
require_file "$WORKDIR/$CORE1.dx"
206+
PQR2XML="$(resolve_tool pqr2xml)"
207+
MAKE_RXN_PAIRS="$(resolve_tool make_rxn_pairs)"
208+
MAKE_RXN_FILE="$(resolve_tool make_rxn_file)"
209+
BD_TOP="$(resolve_tool bd_top)"
210+
211+
cd "$WORKDIR"
212+
213+
if [[ -z "$DEBYE_LENGTH" ]]; then
214+
if ! DEBYE_LENGTH="$(infer_debye_length)"; then
215+
printf 'Could not infer DEBYE_LENGTH from APBS logs. Set DEBYE_LENGTH explicitly.\n' >&2
216+
exit 1
217+
fi
218+
fi
219+
220+
echo "=== 1. PQR -> BrownDye XML ==="
221+
"$PQR2XML" <"$CORE0.pqr" >"${CORE0}_atoms.xml"
222+
"$PQR2XML" <"$CORE1.pqr" >"${CORE1}_atoms.xml"
223+
224+
echo "=== 2. Reaction criteria ==="
225+
write_contact_types
226+
"$MAKE_RXN_PAIRS" -nonred \
227+
-mol0 "${CORE0}_atoms.xml" \
228+
-mol1 "${CORE1}_atoms.xml" \
229+
-ctypes contact_types.xml \
230+
-dist "$RXN_SEARCH_DISTANCE" >reaction_pairs.xml
231+
"$MAKE_RXN_FILE" \
232+
-pairs reaction_pairs.xml \
233+
-state_from before \
234+
-state_to after \
235+
-rxn association \
236+
-mol0 "$CORE0" "$CORE0" \
237+
-mol1 "$CORE1" "$CORE1" \
238+
-distance "$RXN_DISTANCE" \
239+
-nneeded "$RXN_NEEDED" >reactions.xml
240+
241+
echo "=== 3. BrownDye top-level input ==="
242+
write_input_xml
243+
"$BD_TOP" input.xml
244+
245+
echo "=== Done ==="
246+
echo "Simulation XML should be ${CORE0}_${CORE1}_simulation.xml"

examples/browndye/bdrun.sh

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/usr/bin/env bash
2+
# bdrun.sh - run BrownDye and compute association-rate estimates.
3+
#
4+
# Prerequisite:
5+
# cd examples/browndye
6+
# bash bdprep.sh
7+
#
8+
# Usage:
9+
# bash bdrun.sh
10+
# MODE=we bash bdrun.sh
11+
12+
set -euo pipefail
13+
14+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15+
WORKDIR="${WORKDIR:-$SCRIPT_DIR/tmp}"
16+
BD_HOME="${BD_HOME:-/apps/browndye2}"
17+
BD_BIN="${BD_BIN:-$BD_HOME/bin}"
18+
BD_AUX="${BD_AUX:-$BD_HOME/aux}"
19+
20+
CORE0="${CORE0:-protein}"
21+
CORE1="${CORE1:-ligand}"
22+
SIMULATION_XML="${SIMULATION_XML:-${CORE0}_${CORE1}_simulation.xml}"
23+
RESULTS_FILE="${RESULTS_FILE:-results.xml}"
24+
MODE="${MODE:-nam}"
25+
26+
require_file() {
27+
local path="$1"
28+
if [[ ! -s "$path" ]]; then
29+
printf 'Missing required file: %s\n' "$path" >&2
30+
exit 1
31+
fi
32+
}
33+
34+
resolve_tool() {
35+
local tool="$1"
36+
if [[ -x "$BD_BIN/$tool" ]]; then
37+
printf '%s\n' "$BD_BIN/$tool"
38+
return
39+
fi
40+
if [[ -x "$BD_AUX/$tool" ]]; then
41+
printf '%s\n' "$BD_AUX/$tool"
42+
return
43+
fi
44+
if command -v "$tool" >/dev/null 2>&1; then
45+
command -v "$tool"
46+
return
47+
fi
48+
49+
printf 'Missing BrownDye tool: %s\n' "$tool" >&2
50+
printf 'Checked BD_BIN=%s, BD_AUX=%s, and PATH.\n' "$BD_BIN" "$BD_AUX" >&2
51+
exit 1
52+
}
53+
54+
cd "$WORKDIR"
55+
require_file "$SIMULATION_XML"
56+
57+
case "$MODE" in
58+
nam)
59+
NAM_SIMULATION="$(resolve_tool nam_simulation)"
60+
COMPUTE_RATE_CONSTANT="$(resolve_tool compute_rate_constant)"
61+
echo "=== BrownDye NAM simulation ==="
62+
"$NAM_SIMULATION" "$SIMULATION_XML"
63+
require_file "$RESULTS_FILE"
64+
"$COMPUTE_RATE_CONSTANT" <"$RESULTS_FILE" | tee rate_constant.txt
65+
;;
66+
we)
67+
BUILD_BINS="$(resolve_tool build_bins)"
68+
WE_SIMULATION="$(resolve_tool we_simulation)"
69+
COMPUTE_RATE_CONSTANT_WE="$(resolve_tool compute_rate_constant_we)"
70+
echo "=== BrownDye weighted-ensemble simulation ==="
71+
"$BUILD_BINS" "$SIMULATION_XML"
72+
"$WE_SIMULATION" "$SIMULATION_XML"
73+
require_file "$RESULTS_FILE"
74+
"$COMPUTE_RATE_CONSTANT_WE" <"$RESULTS_FILE" | tee rate_constant_we.txt
75+
;;
76+
*)
77+
printf 'Unknown MODE: %s (expected nam or we)\n' "$MODE" >&2
78+
exit 1
79+
;;
80+
esac
81+
82+
echo "=== Done ==="

0 commit comments

Comments
 (0)