Skip to content

Commit 154aa5a

Browse files
authored
Update for RDKit 2024.9 (#51)
Dependencies - Add support for Python 3.13 - Drop support for Python 3.8 - Require RDKit version 2024.9 as a minimum Energy ratio module - Update energy ratio tests to be compatible with InChI 1.07 Development - Add GitHub stale action
1 parent 6062b99 commit 154aa5a

16 files changed

+149
-40
lines changed

.github/workflows/inactivity.yml

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Inactivity
2+
3+
on:
4+
workflow_dispatch:
5+
# push:
6+
# branches: [main]
7+
# pull_request:
8+
schedule:
9+
# ┌───────────── minute (0 - 59)
10+
# │ ┌───────────── hour (0 - 23)
11+
# │ │ ┌───────────── day of the month (1 - 31)
12+
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
13+
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
14+
# │ │ │ │ │
15+
# │ │ │ │ │
16+
# │ │ │ │ │
17+
# * * * * *
18+
- cron: '47 2 3 * *'
19+
20+
permissions:
21+
issues: write
22+
pull-requests: write
23+
24+
jobs:
25+
close-issues:
26+
runs-on: ubuntu-latest
27+
28+
steps:
29+
30+
- name: Close inactive issues, PRs, and stale issues
31+
uses: actions/stale@v3
32+
with:
33+
repo-token: "${{ secrets.GITHUB_TOKEN }}"
34+
days-before-stale: 60
35+
days-before-close: 7
36+
# Issues
37+
stale-issue-message: "This issue has been automatically closed due to inactivity. Please feel free to reopen it if you still have this problem."
38+
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
39+
stale-issue-label: "stale"
40+
exempt-issue-labels: "in-progress"
41+
# PRs
42+
stale-pr-label: "stale"
43+
exempt-all-pr-assignees: true
44+
stale-pr-message: "This PR was marked as stale because it has been open for 60 days with no activity."
45+
close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale."

.github/workflows/testing.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
fail-fast: true
1515
matrix:
1616
os: [ubuntu-latest, macos-latest, windows-latest]
17-
python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
17+
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
1818

1919
steps:
2020

posebusters/cli.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Command line interface for PoseBusters."""
2+
23
from __future__ import annotations
34

45
import argparse
56
import logging
67
import sys
8+
from collections.abc import Iterable
79
from pathlib import Path
8-
from typing import Any, Iterable, TextIO
10+
from typing import Any, TextIO
911

1012
import pandas as pd
1113
from rdkit.Chem.rdchem import Mol

posebusters/modules/distance_geometry.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Module to check bond lengths, bond angles, and internal clash of ligand conformations."""
2+
23
from __future__ import annotations
34

5+
from collections.abc import Iterable
46
from copy import deepcopy
57
from logging import getLogger
6-
from typing import Any, Iterable
8+
from typing import Any
79

810
import numpy as np
911
import pandas as pd

posebusters/modules/energy_ratio.py

+48-21
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
import logging
66
from copy import deepcopy
7-
from functools import lru_cache
7+
from functools import cache
8+
from math import isfinite
89

9-
import numpy as np
1010
from rdkit import ForceField # noqa: F401
1111
from rdkit.Chem.inchi import InchiReadWriteError, MolFromInchi
1212
from rdkit.Chem.rdchem import Mol
@@ -23,13 +23,14 @@
2323

2424
logger = logging.getLogger(__name__)
2525

26+
2627
_warning_prefix = "WARNING: Energy ratio module "
2728
_empty_results = {
2829
"results": {
29-
"ensemble_avg_energy": np.nan,
30-
"mol_pred_energy": np.nan,
31-
"energy_ratio": np.nan,
32-
"energy_ratio_passes": np.nan,
30+
"ensemble_avg_energy": float("nan"),
31+
"mol_pred_energy": float("nan"),
32+
"energy_ratio": float("nan"),
33+
"energy_ratio_passes": float("nan"),
3334
}
3435
}
3536

@@ -39,7 +40,8 @@ def check_energy_ratio(
3940
threshold_energy_ratio: float = 7.0,
4041
ensemble_number_conformations: int = 100,
4142
inchi_strict: bool = False,
42-
):
43+
epsilon=1e-10,
44+
) -> dict[str, dict[str, float | bool]]:
4345
"""Check whether the energy of the docked ligand is within user defined range.
4446
4547
Args:
@@ -72,40 +74,65 @@ def check_energy_ratio(
7274
return _empty_results
7375

7476
try:
75-
conf_energy = get_conf_energy(mol_pred)
77+
observed_energy = get_conf_energy(mol_pred)
7678
except Exception as e:
7779
logger.warning(_warning_prefix + "failed to calculate conformation energy for %s: %s", inchi, e)
78-
conf_energy = np.nan
80+
observed_energy = float("nan")
7981

8082
try:
81-
avg_energy = float(get_average_energy(inchi, ensemble_number_conformations))
83+
energies = get_energies(inchi, ensemble_number_conformations)
84+
mean_energy = sum(energies) / len(energies)
85+
std_energy = sum((energy - mean_energy) ** 2 for energy in energies) / (len(energies) - 1)
86+
std_energy = max(epsilon, std_energy) # clipping
8287
except Exception as e:
8388
logger.warning(_warning_prefix + "failed to calculate ensemble conformation energy for %s: %s", inchi, e)
84-
avg_energy = np.nan
89+
mean_energy = float("nan")
90+
std_energy = float("nan")
8591

86-
if avg_energy == 0:
92+
if mean_energy == 0:
8793
logger.warning(_warning_prefix + "calculated average energy of molecule 0 for %s", inchi)
88-
avg_energy = np.nan
94+
mean_energy = epsilon # clipping
95+
96+
# simple ratio
97+
ratio = observed_energy / mean_energy
98+
ratio_passes = ratio <= threshold_energy_ratio if isfinite(ratio) else float("nan")
8999

90-
pred_factor = conf_energy / avg_energy
91-
ratio_passes = pred_factor <= threshold_energy_ratio
100+
# ratio after subtracting mean
101+
deviation = observed_energy - mean_energy
102+
relative_deviation = deviation / mean_energy
103+
relative_deviation_passes = (
104+
relative_deviation <= threshold_energy_ratio if isfinite(relative_deviation) else float("nan")
105+
)
106+
107+
# standard score (ratio after subtracting by population mean and dividing by population std)
108+
z_value = (observed_energy - mean_energy) / std_energy
109+
z_value_passes = z_value <= threshold_energy_ratio if isfinite(z_value) else float("nan")
92110

93111
results = {
94-
"ensemble_avg_energy": avg_energy,
95-
"mol_pred_energy": conf_energy,
96-
"energy_ratio": pred_factor,
112+
"ensemble_avg_energy": mean_energy,
113+
"mol_pred_energy": observed_energy,
114+
"energy_ratio": ratio,
115+
"relative_deviation": relative_deviation,
116+
"z_value": z_value,
97117
"energy_ratio_passes": ratio_passes,
118+
"relative_deviation_passes": relative_deviation_passes,
119+
"z_value_passes": z_value_passes,
98120
}
99121
return {"results": results}
100122

101123

102-
@lru_cache(maxsize=None)
103124
def get_average_energy(inchi: str, n_confs: int = 50, num_threads: int = 0) -> Mol:
104125
"""Get average energy of an ensemble of molecule conformations."""
126+
energies = get_energies(inchi, n_confs, num_threads)
127+
return sum(energies) / len(energies)
128+
129+
130+
@cache
131+
def get_energies(inchi: str, n_confs: int = 50, num_threads: int = 0) -> list[float]:
132+
"""Get energies of an ensemble of molecule conformations."""
105133
with CaptureLogger():
106134
mol = MolFromInchi(inchi)
107-
energies = new_conformation(mol, n_confs, num_threads)["energies"]
108-
return sum(energies) / len(energies)
135+
return new_conformation(mol, n_confs, num_threads)["energies"]
109136

110137

111138
def new_conformation(mol: Mol, n_confs: int = 1, num_threads: int = 0, energy_minimization=True) -> Mol:

posebusters/modules/flatness.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Module to check flatness of ligand substructures."""
2+
23
from __future__ import annotations
34

5+
from collections.abc import Iterable
46
from copy import deepcopy
5-
from typing import Any, Iterable
7+
from typing import Any
68

79
import numpy as np
810
from numpy import ndarray as Array

posebusters/modules/loading.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Module to check loading of protein and ligand files."""
2+
23
from __future__ import annotations
34

45
from typing import Any

posebusters/modules/rmsd.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Module to check RMSD between docked and crystal ligand."""
2+
23
from __future__ import annotations
34

45
import logging

posebusters/modules/volume_overlap.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Module to check volume overlap between docked ligand and protein."""
2+
23
from __future__ import annotations
34

45
import logging

posebusters/posebusters.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import inspect
66
import logging
77
from collections import defaultdict
8+
from collections.abc import Generator, Iterable
89
from functools import partial
910
from pathlib import Path
10-
from typing import Any, Callable, Generator, Iterable
11+
from typing import Any, Callable
1112

1213
import pandas as pd
1314
from rdkit.Chem.rdchem import Mol

posebusters/tools/loading.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from __future__ import annotations
44

55
import logging
6+
from collections.abc import Generator
67
from pathlib import Path
7-
from typing import Generator
88

99
from rdkit.Chem.AllChem import AssignBondOrdersFromTemplate
1010
from rdkit.Chem.rdchem import Mol

posebusters/tools/molecules.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Iterable
56
from copy import deepcopy
67
from logging import getLogger
7-
from typing import Iterable
88

99
import numpy as np
1010
from rdkit import RDLogger

posebusters/tools/parallel.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Iterable
56
from multiprocessing import Pool
6-
from typing import Callable, Iterable
7+
from typing import Callable
78

89
from tqdm import tqdm
910

posebusters/tools/protein.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Protein related functions."""
2+
23
from __future__ import annotations
34

45
import logging
5-
from typing import Iterable
6+
from collections.abc import Iterable
67

78
from rdkit.Chem.rdchem import Atom, Mol
89

pyproject.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ classifiers = [
1616
"Intended Audience :: Science/Research",
1717
"License :: OSI Approved :: BSD License",
1818
"Programming Language :: Python :: 3 :: Only",
19-
"Programming Language :: Python :: 3.8",
2019
"Programming Language :: Python :: 3.9",
2120
"Programming Language :: Python :: 3.10",
2221
"Programming Language :: Python :: 3.11",
2322
"Programming Language :: Python :: 3.12",
23+
"Programming Language :: Python :: 3.13",
2424
"Operating System :: OS Independent",
2525
]
26-
requires-python = "~=3.8"
26+
requires-python = "~=3.9"
2727
dynamic = ["version", "description"]
28-
dependencies = ['rdkit >= 2020.09', 'pandas', 'numpy', 'pyyaml']
28+
dependencies = ['rdkit >= 2024.9', 'pandas', 'numpy', 'pyyaml']
2929

3030
[project.optional-dependencies]
3131
dev = ["ruff"]
+32-7
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,63 @@
11
from __future__ import annotations
22

3+
import math
4+
35
import numpy as np
46

57
from posebusters.modules.energy_ratio import check_energy_ratio
68

79

810
def test_check_energy_ratio(mol_pm2):
11+
# molecule has valid conformation
912
out = check_energy_ratio(mol_pm2)
13+
assert math.isfinite(out["results"]["mol_pred_energy"])
14+
assert math.isfinite(out["results"]["ensemble_avg_energy"])
15+
assert math.isfinite(out["results"]["energy_ratio"])
1016
assert out["results"]["energy_ratio_passes"] is True
1117

1218

1319
def test_check_energy_ratio_14gs_0(mol_pred_14gs_gen0):
14-
# nan because molecule cannot be converted to valid InChI
20+
# molecule is chemically insane, but conformation looks alright
1521
out = check_energy_ratio(mol_pred_14gs_gen0)
22+
assert math.isfinite(out["results"]["mol_pred_energy"])
23+
assert math.isfinite(out["results"]["ensemble_avg_energy"])
24+
assert math.isfinite(out["results"]["energy_ratio"])
1625
assert out["results"]["energy_ratio_passes"] is True
1726

1827

1928
def test_check_energy_ratio_1afs_87(mol_pred_1afs_gen87):
2029
# nan because molecule cannot be converted to valid InChI
2130
out = check_energy_ratio(mol_pred_1afs_gen87)
22-
assert np.isnan(out["results"]["energy_ratio_passes"])
31+
assert math.isnan(out["results"]["mol_pred_energy"])
32+
assert math.isnan(out["results"]["ensemble_avg_energy"])
33+
assert math.isnan(out["results"]["energy_ratio"])
34+
assert math.isnan(out["results"]["energy_ratio_passes"])
2335

2436

2537
def test_check_energy_ratio_1afs_94(mol_pred_1afs_gen94):
26-
# nan because molecule cannot be converted to valid InChI
38+
# molecule is chemically insane, but conformation looks alright
2739
out = check_energy_ratio(mol_pred_1afs_gen94)
28-
assert out["results"]["energy_ratio_passes"] is True
40+
assert math.isfinite(out["results"]["mol_pred_energy"])
41+
assert math.isnan(out["results"]["ensemble_avg_energy"])
42+
assert math.isnan(out["results"]["energy_ratio"])
43+
assert math.isnan(out["results"]["energy_ratio_passes"])
2944

3045

3146
def test_check_energy_ratio_1jn2_3(mol_pred_1jn2_gen3):
3247
# nan because molecule cannot be converted to valid InChI
3348
out = check_energy_ratio(mol_pred_1jn2_gen3)
34-
assert np.isnan(out["results"]["energy_ratio_passes"])
49+
assert math.isnan(out["results"]["mol_pred_energy"])
50+
assert math.isnan(out["results"]["ensemble_avg_energy"])
51+
assert math.isnan(out["results"]["energy_ratio"])
52+
assert math.isnan(out["results"]["energy_ratio_passes"])
3553

3654

3755
def test_check_energy_ratio_1jn2_62(mol_pred_1jn2_gen62):
38-
# nan because molecule cannot be converted to valid InChI
56+
# molecule is chemically insane, but conformation looks alright
3957
out = check_energy_ratio(mol_pred_1jn2_gen62)
58+
assert math.isfinite(out["results"]["mol_pred_energy"])
59+
assert math.isfinite(out["results"]["ensemble_avg_energy"])
60+
assert math.isfinite(out["results"]["energy_ratio"])
4061
assert out["results"]["energy_ratio_passes"] is True
4162

4263

@@ -49,5 +70,9 @@ def test_check_energy_ratio_approximate_consistency(mol_1a30_clash_2, mol_1a30_c
4970

5071

5172
def test_check_energy_ratio_disconnected_atoms(mol_disconnnected_atoms):
73+
# no bonds, just disconnected atoms -> energy is 0
5274
out = check_energy_ratio(mol_disconnnected_atoms)
53-
assert out["results"]["energy_ratio_passes"] is False
75+
assert math.isfinite(out["results"]["mol_pred_energy"])
76+
assert math.isfinite(out["results"]["ensemble_avg_energy"])
77+
assert math.isfinite(out["results"]["energy_ratio"])
78+
assert out["results"]["energy_ratio_passes"] is True

0 commit comments

Comments
 (0)