Skip to content

Commit 6261eff

Browse files
authored
Merge pull request #1623 from haddocking/reverse-selection
Added an option to select models based on a reverse ranking
2 parents 7ad1148 + be017a8 commit 6261eff

8 files changed

Lines changed: 259 additions & 70 deletions

File tree

CHANGELOG.md

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

33
=======
4+
- 2026-07-13: Added option to select models/clusters in reverse order - Issue #1620
45
- 2026-07-10: Corrected ranking in caprieval for reverse sorting - Issue #1621
56
- 2026-07-09: Added `deeprank` scoring module using deeprank-gnn-esm - Issue #569
67
- 2026-07-07: Re-add `gdock` as a sampling module

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,16 @@ def _run(self) -> None:
4949
models_to_select: list[PDBFile] = [
5050
p for p in self.previous_io.output if p.file_type == Format.PDB
5151
]
52+
if self.params["sort_ascending"]:
53+
reverse = False
54+
self.log("Selecting models with the lowest score")
55+
else:
56+
reverse = True
57+
if reverse:
58+
self.log("Selecting models with the highest score")
5259

5360
# sort the models based on their score
54-
models_to_select.sort(key=lambda x: x.score)
61+
models_to_select.sort(key=lambda x: x.score, reverse=reverse)
5562

5663
if len(models_to_select) < self.params["select"]:
5764
self.log(

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@ select:
88
long: Number of best-ranked models to select for further steps of the workflow. Typically only 200 models produced at the rigid-body docking stage are selected for further refinement.
99
group: analysis
1010
explevel: easy
11+
sort_ascending:
12+
default: true
13+
type: boolean
14+
title: Sort in ascending order
15+
short: Sort in ascending order.
16+
long: Sort in ascending order. “sort_ascending = false” should be used when a higher score corresponds to a better predicted model quality.
17+
group: analysis
18+
explevel: easy

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from haddock.modules.analysis.seletopclusts.seletopclusts import (
2222
select_top_clusts_models,
2323
write_selected_models,
24-
)
24+
)
2525

2626

2727
RECIPE_PATH = Path(__file__).resolve().parent
@@ -33,12 +33,14 @@ class HaddockModule(BaseHaddockModule):
3333

3434
name = RECIPE_PATH.name
3535

36-
def __init__(self,
37-
order: int,
38-
path: Path,
39-
*ignore: Any,
40-
init_params: FilePath = DEFAULT_CONFIG,
41-
**everything: Any) -> None:
36+
def __init__(
37+
self,
38+
order: int,
39+
path: Path,
40+
*ignore: Any,
41+
init_params: FilePath = DEFAULT_CONFIG,
42+
**everything: Any,
43+
) -> None:
4244
super().__init__(order, path, init_params)
4345

4446
@classmethod
@@ -57,6 +59,15 @@ def _run(self) -> None:
5759
_msg = "top_clusters must be an integer."
5860
self.finish_with_error(_msg)
5961

62+
# The reverse option only applies to score-based ranking
63+
reverse = not self.params["sort_ascending"]
64+
if reverse and self.params["sortby"] != "size":
65+
self.log("Selecting clusters with the highest score first")
66+
elif not reverse and self.params["sortby"] != "size":
67+
self.log("Selecting clusters with the lowest score first")
68+
else:
69+
self.log("Selecting clusters with the largest size first")
70+
self.log("Selecting clusters with the lowest score first")
6071
# Retrieve list of previous models
6172
models_to_select = self.previous_io.retrieve_models()
6273

@@ -65,16 +76,18 @@ def _run(self) -> None:
6576
_msg = (
6677
"Impossible to obtain cluster information. Please consider "
6778
"running a clustering method prior to this module."
68-
)
79+
)
6980
self.finish_with_error(_msg)
7081

7182
# Make model selection
7283
selected_models, _notes = select_top_clusts_models(
7384
self.params["sortby"],
85+
reverse,
7486
models_to_select,
7587
self.params["top_clusters"],
7688
self.params["top_models"],
77-
)
89+
self.params["cluster_score_threshold"],
90+
)
7891
# Log notes
7992
for note in _notes:
8093
log.info(note)
@@ -84,7 +97,7 @@ def _run(self) -> None:
8497
"seletopclusts.txt",
8598
selected_models,
8699
self.path,
87-
)
100+
)
88101

89102
# Make these new models the output of this module
90103
self.output_models = renamed_models

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,21 @@ sortby:
3131
long: if the selection is done by 'score' the average score of the top (4) models of each cluster is used to define the cluster rank. When clustering by 'size', a bigger cluster size corresponds to a higher rank. By default, 'score' is selected.
3232
group: analysis
3333
explevel: easy
34+
sort_ascending:
35+
default: true
36+
type: boolean
37+
title: Sort in ascending order
38+
short: Sort in ascending order.
39+
long: Sort in ascending order. “sort_ascending = false” should be used when a higher score corresponds to a better predicted model quality and is only used if “sortby” is set to "score".
40+
group: analysis
41+
explevel: easy
42+
cluster_score_threshold:
43+
default: 4
44+
type: integer
45+
min: 1
46+
max: 99999
47+
title: Number of models used to compute a cluster's average score
48+
short: Number of best-scoring models per cluster used to compute its average score. By default 4.
49+
long: When ranking clusters by 'score', the average score of this many best-scoring models of each cluster is used to define the cluster rank. By default 4.
50+
group: analysis
51+
explevel: easy

src/haddock/modules/analysis/seletopclusts/seletopclusts.py

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
import math
55
from pathlib import Path
66
from haddock.core.typing import Union
7+
from haddock.libs.libclust import rank_clusters
78
from haddock.libs.libontology import PDBFile
89

910

1011
def select_top_clusts_models(
11-
sortby: str,
12-
models_to_select: list[PDBFile],
13-
top_clusters: int,
14-
top_models: Union[int, float],
15-
) -> tuple[list[PDBFile], list[str]]:
12+
sortby: str,
13+
reverse: bool,
14+
models_to_select: list[PDBFile],
15+
top_clusters: int,
16+
top_models: Union[int, float],
17+
cluster_score_threshold: int = 4,
18+
) -> tuple[list[PDBFile], list[str]]:
1619
"""Select best clusters based on structures scores.
1720
1821
Parameters
@@ -25,6 +28,9 @@ def select_top_clusts_models(
2528
Number of best clusters to take into account.
2629
top_models : int
2730
Number of best models in each cluster to take into account.
31+
cluster_score_threshold : int
32+
Number of best-scoring models per cluster used to compute its
33+
average score when ranking by `score`.
2834
2935
Returns
3036
-------
@@ -40,7 +46,11 @@ def select_top_clusts_models(
4046
if sortby == "size":
4147
cluster_rankings = size_clust_order(by_clusters)
4248
else:
43-
cluster_rankings = rank_clust_order(by_clusters)
49+
cluster_rankings = rank_clust_order(
50+
by_clusters,
51+
reverse,
52+
cluster_score_threshold,
53+
)
4454

4555
# Check if number of clusters >= set of rank
4656
if top_clusters >= len(cluster_rankings):
@@ -51,23 +61,22 @@ def select_top_clusts_models(
5161
# select top_clusters clusters
5262
cluster_rankings = cluster_rankings[:top_clusters]
5363
cluster_rankins_str = ",".join(map(str, cluster_rankings))
54-
notes.append(
55-
f"Selecting top {top_clusters} clusters: "
56-
f"{cluster_rankins_str}"
57-
)
64+
notes.append(f"Selecting top {top_clusters} clusters: {cluster_rankins_str}")
5865

5966
# Initiate set of selected models to export
6067
models_to_export: list[PDBFile] = []
61-
# Loop over cluster ranks
62-
for clt_rank in cluster_rankings:
68+
# Loop over cluster ranks. `new_clt_rank` reflects the ordering computed
69+
# above (by average score or size), so the exported model names follow
70+
# that ranking rather than the original cluster rank.
71+
for new_clt_rank, clt_key in enumerate(cluster_rankings, start=1):
6372
# Sort models by model rank
64-
clt_mdls, note = sort_models(by_clusters[clt_rank])
73+
clt_mdls, note = sort_models(by_clusters[clt_key])
6574
if note:
6675
notes.append(note)
6776

6877
# Set new ranks to models
6978
for mdl_rank, pdb in enumerate(clt_mdls, start=1):
70-
pdb.clt_rank = clt_rank
79+
pdb.clt_rank = new_clt_rank
7180
pdb.clt_model_rank = mdl_rank
7281
# In case number of models is not set (nan.)
7382
if math.isnan(top_models):
@@ -81,9 +90,7 @@ def select_top_clusts_models(
8190
return models_to_export, notes
8291

8392

84-
def sort_models(
85-
models: list[PDBFile]
86-
) -> tuple[list[PDBFile], Union[None, str]]:
93+
def sort_models(models: list[PDBFile]) -> tuple[list[PDBFile], Union[None, str]]:
8794
"""Sort models based on their rank in cluster.
8895
8996
Parameters
@@ -101,16 +108,18 @@ def sort_models(
101108
sorted_mdls = sorted(
102109
models,
103110
key=lambda k: k.clt_model_rank,
104-
)
111+
)
105112
except TypeError:
106-
note = 'model rank unavailable, falling back to input order'
113+
note = "model rank unavailable, falling back to input order"
107114
sorted_mdls = models
108115
return sorted_mdls, note
109-
116+
110117

111118
def rank_clust_order(
112-
by_clusters: dict[int, list[PDBFile]],
113-
) -> list[int]:
119+
by_clusters: dict[int, list[PDBFile]],
120+
reverse: bool = False,
121+
cluster_score_threshold: int = 4,
122+
) -> list[int]:
114123
"""Select best clusters based on structures scores.
115124
116125
Parameters
@@ -121,6 +130,11 @@ def rank_clust_order(
121130
Number of best clusters to take into account.
122131
top_models : int
123132
Number of best models in each cluster to take into account.
133+
reverse : boolean
134+
Reverse the ranking (from high to low average score)
135+
cluster_score_threshold : int
136+
Number of best-scoring models per cluster used to compute its
137+
average score.
124138
125139
Returns
126140
-------
@@ -129,14 +143,20 @@ def rank_clust_order(
129143
notes : list[str]
130144
List of notes to be printed.
131145
"""
132-
# Generate set of all cluster rank available
133-
cluster_rankings = sorted(by_clusters)
146+
# Rank clusters by their average score (best/lowest score first)
147+
_score_dic, sorted_score_dic = rank_clusters(
148+
by_clusters,
149+
cluster_score_threshold,
150+
)
151+
cluster_rankings = [clt_key for clt_key, _score in sorted_score_dic]
152+
if reverse:
153+
cluster_rankings.reverse()
134154
return cluster_rankings
135155

136156

137157
def size_clust_order(
138-
by_clusters: dict[int, list[PDBFile]],
139-
) -> list[int]:
158+
by_clusters: dict[int, list[PDBFile]],
159+
) -> list[int]:
140160
"""Select best clusters based on structures scores.
141161
142162
Parameters
@@ -160,7 +180,7 @@ def size_clust_order(
160180
by_clusters,
161181
key=lambda k: len(by_clusters[k]),
162182
reverse=True,
163-
)
183+
)
164184
return cluster_rankings
165185

166186

@@ -179,9 +199,8 @@ def map_clusters_models(models: list[PDBFile]) -> dict[int, list[PDBFile]]:
179199
"""
180200
# Preset dictionary keys
181201
by_clusters: dict[int, list[PDBFile]] = {
182-
clrank: []
183-
for clrank in list(set([pdb.clt_rank for pdb in models]))
184-
}
202+
clrank: [] for clrank in list(set([pdb.clt_rank for pdb in models]))
203+
}
185204
# Loop over models
186205
for pdb in models:
187206
# Add model to cluster
@@ -190,10 +209,10 @@ def map_clusters_models(models: list[PDBFile]) -> dict[int, list[PDBFile]]:
190209

191210

192211
def write_selected_models(
193-
output_path: Union[str, Path],
194-
models: list[PDBFile],
195-
module_path: Union[str, Path],
196-
) -> list[PDBFile]:
212+
output_path: Union[str, Path],
213+
models: list[PDBFile],
214+
module_path: Union[str, Path],
215+
) -> list[PDBFile]:
197216
"""Dump selected models and new names in a file.
198217
199218
Parameters
@@ -211,26 +230,20 @@ def write_selected_models(
211230
Updated list of selected models.
212231
"""
213232
# dump the models to disk and change their attributes
214-
with open(output_path, 'w') as fh:
233+
with open(output_path, "w") as fh:
215234
fh.write("rel_path\tori_name\tcluster_name\tmd5" + os.linesep)
216235
for model in models:
217-
name = (
218-
f"cluster_{model.clt_rank}_model"
219-
f"_{model.clt_model_rank}.pdb"
220-
)
236+
name = f"cluster_{model.clt_rank}_model_{model.clt_model_rank}.pdb"
221237
# writing name
222238
fh.write(
223-
f"{model.rel_path}\t"
224-
f"{model.ori_name}\t"
225-
f"{name}\t"
226-
f"{model.md5}" + os.linesep
227-
)
239+
f"{model.rel_path}\t{model.ori_name}\t{name}\t{model.md5}" + os.linesep
240+
)
228241
# changing attributes
229242
name_path = Path(name)
230243
name_path.write_text(model.rel_path.read_text())
231244
model.ori_name = model.file_name
232245
model.file_name = name
233246
model.full_name = name
234-
model.rel_path = Path('..', Path(module_path).name, name)
247+
model.rel_path = Path("..", Path(module_path).name, name)
235248
model.path = str(Path(".").resolve())
236249
return models

0 commit comments

Comments
 (0)