Skip to content

Commit 1b37c73

Browse files
authored
Merge pull request #1412 from haddocking/alascan-ligand
Alascan with non-standard ligand/molecules/residues
2 parents 26fd0c5 + 9a024f1 commit 1b37c73

File tree

7 files changed

+578
-41
lines changed

7 files changed

+578
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
- 2025-10-30: Added possibility to use alascan with ligands - Issue #1411
34
- 2025-10-22: Allow the definition of chain combinations to be used for scoring - Issue #1414
45
- 2025-09-11: Added `grid` mode
56
- 2025-09-11: Corrected antibody-antigen notebook - Issue #1383

integration_tests/golden_data/protlig_complex_1.pdb

Lines changed: 354 additions & 0 deletions
Large diffs are not rendered by default.

integration_tests/test_alascan.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ def alascan_module():
2727
yield alascan
2828

2929

30+
@pytest.fixture
31+
def alascan_module_protlig():
32+
"""Return a default alascan module."""
33+
with tempfile.TemporaryDirectory() as tmpdir:
34+
alascan = AlascanModule(
35+
order=0, path=Path(tmpdir), initial_params=DEFAULT_ALASCAN_CONFIG
36+
)
37+
alascan.params["int_cutoff"] = 3.5
38+
alascan.params["output_mutants"] = True
39+
# Copy parameters and toplogy of the ligand
40+
shutil.copy(
41+
Path(GOLDEN_DATA, "ligand.top"),
42+
Path(alascan.path, "ligand.top"),
43+
)
44+
shutil.copy(
45+
Path(GOLDEN_DATA, "ligand.param"),
46+
Path(alascan.path, "ligand.param"),
47+
)
48+
# Set the parameters to point the file
49+
alascan.params["ligand_param_fname"] = Path(alascan.path, "ligand.param")
50+
alascan.params["ligand_top_fname"] = Path(alascan.path, "ligand.top")
51+
yield alascan
52+
53+
3054
class MockPreviousIO:
3155
def __init__(self, path):
3256
self.path = path
@@ -70,6 +94,26 @@ def output(self):
7094
return None
7195

7296

97+
class MockPreviousIO_protlig:
98+
def __init__(self, path):
99+
self.path = path
100+
101+
def retrieve_models(self, individualize: bool = False):
102+
fname = "protlig_complex_1.pdb"
103+
shutil.copy(
104+
Path(GOLDEN_DATA, fname),
105+
Path(self.path, fname),
106+
)
107+
model_list = [
108+
PDBFile(file_name=fname, path=self.path),
109+
]
110+
111+
return model_list
112+
113+
def output(self):
114+
return None
115+
116+
73117
def test_alascan_default(alascan_module, mocker):
74118
"""Test the alascan module."""
75119
alascan_module.previous_io = MockPreviousIO(path=alascan_module.path)
@@ -148,3 +192,47 @@ def test_alascan_mutation_resiudes():
148192
config_allowed_resiudes = set(default_config["scan_residue"]["choices"])
149193
script_allowed_resiudes = set(list(RES_CODES.keys()))
150194
assert config_allowed_resiudes == script_allowed_resiudes
195+
196+
197+
def test_alascan_with_ligand_topar(alascan_module_protlig, mocker):
198+
"""Test the use of alascan in presence of a ligand."""
199+
alascan_module_protlig.previous_io = MockPreviousIO_protlig(path=alascan_module_protlig.path)
200+
alascan_module_protlig.run()
201+
202+
expected_csv = Path(alascan_module_protlig.path, "scan_protlig_complex_1.tsv")
203+
expected_clt_csv = Path(alascan_module_protlig.path, "scan_clt_unclustered.tsv")
204+
205+
assert expected_csv.exists(), f"{expected_csv} does not exist"
206+
assert expected_clt_csv.exists(), f"{expected_clt_csv} does not exist"
207+
208+
# List mutated files
209+
mutated_filepaths = list(Path(alascan_module_protlig.path).glob("protlig_complex_1-*.pdb"))
210+
assert len(mutated_filepaths) >= 1
211+
212+
# Loop over files
213+
for mutated_fpath in mutated_filepaths:
214+
# Make sure the ligand is in it
215+
file_content = mutated_fpath.read_text()
216+
assert file_content.count("G39") > 20
217+
218+
219+
def test_alascan_without_ligand_topar(alascan_module, mocker):
220+
"""Test the use of alascan in presence of a ligand without topo/param."""
221+
alascan_module.previous_io = MockPreviousIO_protlig(path=alascan_module.path)
222+
alascan_module.run()
223+
224+
expected_csv = Path(alascan_module.path, "scan_protlig_complex_1.tsv")
225+
expected_clt_csv = Path(alascan_module.path, "scan_clt_unclustered.tsv")
226+
227+
assert expected_csv.exists(), f"{expected_csv} does not exist"
228+
assert expected_clt_csv.exists(), f"{expected_clt_csv} does not exist"
229+
230+
# List mutated files
231+
mutated_filepaths = list(Path(alascan_module.path).glob("protlig_complex_1-*.pdb"))
232+
assert len(mutated_filepaths) >= 1
233+
234+
# Loop over files
235+
for mutated_fpath in mutated_filepaths:
236+
# Make sure the ligand is not in it
237+
file_content = mutated_fpath.read_text()
238+
assert file_content.count("G39") == 0

src/haddock/clis/cli_score.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def main(
149149
from contextlib import suppress
150150
from pathlib import Path
151151

152-
from haddock import log
152+
from haddock import log, EmptyPath
153153
from haddock.gear.haddockmodel import HaddockModel
154154
from haddock.gear.yaml2cfg import read_from_yaml_config
155155
from haddock.gear.zerofill import zero_fill
@@ -168,23 +168,29 @@ def main(
168168
ems_dict = default_emscoring.copy()
169169
n_warnings = 0
170170
for param, value in kwargs.items():
171+
# Check if the parameter name is in the emscoring module ones
171172
if param not in default_emscoring:
172173
sys.exit(
173174
f"* ERROR * Parameter {param!r} is not a "
174175
f"valid `emscoring` parameter.{os.linesep}"
175-
f"Valid emscoring parameters are: {', '.join(sorted(default_emscoring))}"
176+
"Valid emscoring parameters are: "
177+
f"{', '.join(sorted(default_emscoring))}"
176178
)
179+
# Compare the user-given value to the default one
177180
if value != default_emscoring[param]:
178181
print(
179-
f"* ATTENTION * Value ({value}) of parameter {param} different from default ({default_emscoring[param]})"
180-
) # noqa:E501
182+
f"* ATTENTION * Value ({value}) of parameter {param} "
183+
f"different from default ({default_emscoring[param]})"
184+
)
181185
# get the type of default value
182186
default_type = type(default_emscoring[param])
183-
# convert the value to the same type
187+
# cast the value to the same type
184188
if default_type == bool:
185189
if value.lower() not in ["true", "false"]:
186190
sys.exit(f"* ERROR * Boolean parameter {param} should be True or False")
187191
value = value.lower() == "true"
192+
elif param.endswith("_fname"):
193+
value = EmptyPath() if str(value) == "" else Path(value).resolve()
188194
else:
189195
value = default_type(value)
190196
ems_dict[param] = value
@@ -210,9 +216,14 @@ def main(
210216
# create a copy of the input pdb
211217
input_pdb_copy = Path(tmp.name)
212218
shutil.copy(input_pdb, input_pdb_copy)
213-
214-
params = {
215-
"topoaa": {"molecules": [input_pdb_copy]},
219+
220+
# Setting up a full workflow set of parameters
221+
workflow_params = {
222+
"topoaa": {
223+
"molecules": [input_pdb_copy],
224+
"ligand_param_fname": ems_dict["ligand_param_fname"],
225+
"ligand_top_fname": ems_dict["ligand_top_fname"],
226+
},
216227
"emscoring": ems_dict,
217228
}
218229

@@ -221,13 +232,13 @@ def main(
221232
# run workflow
222233
with working_directory(run_dir):
223234
workflow = WorkflowManager(
224-
workflow_params=params,
235+
workflow_params=workflow_params,
225236
start=0,
226237
run_dir=run_dir,
227238
)
228-
229239
workflow.run()
230240

241+
# Build expected pdb filepath
231242
minimized_mol = Path(run_dir, "1_emscoring", "emscoring_1.pdb")
232243
haddock_score_component_dic = HaddockModel(minimized_mol).energies
233244

@@ -237,6 +248,7 @@ def main(
237248
air = haddock_score_component_dic["air"]
238249
bsa = haddock_score_component_dic["bsa"]
239250

251+
# Compute the haddock score
240252
# emscoring is equivalent to itw
241253
haddock_score_itw = (
242254
ems_dict["w_vdw"] * vdw

src/haddock/modules/analysis/alascan/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def _run(self):
118118
mutation_res=self.params["scan_residue"],
119119
model=model,
120120
params=self.params,
121-
library_mode = False
121+
library_mode=False,
122122
)
123123
for model in models
124124
]
@@ -202,4 +202,4 @@ def _run(self):
202202
# Send models to the next step, no operation is done on them
203203
self.output_models = models
204204

205-
self.export_io_models()
205+
self.export_io_models()

src/haddock/modules/analysis/alascan/defaults.yaml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,21 @@ output_mutants:
6767
short: Dump the mutated, energy-minimized PDB files.
6868
long: Dump the mutated, energy-minimized PDB files. As the number of mutants can be very large, this option is allowed only when a single model is provided in input.
6969
group: analysis
70-
explevel: easy
70+
explevel: easy
71+
ligand_param_fname:
72+
default: ''
73+
type: file
74+
title: Custom ligand parameter file
75+
short: Ligand parameter file in CNS format
76+
long: Ligand parameter file in CNS format, for any ligand/residues/molecules not supported by default by HADDOCK.
77+
group: 'force field'
78+
explevel: easy
79+
ligand_top_fname:
80+
default: ''
81+
type: file
82+
title: Custom ligand topology file
83+
short: Ligand topology file in CNS format
84+
long: Ligand topology file in CNS format containing the ligand topologies
85+
(atoms, masses, charges, bond definitions...) for any ligand not supported by default by HADDOCK
86+
group: 'force field'
87+
explevel: easy

0 commit comments

Comments
 (0)