perf: query the KD-tree once per structure in _identify_bonds - #490
Open
JustinKyleKirkland wants to merge 1 commit into
Open
JustinKyleKirkland wants to merge 1 commit into
JustinKyleKirkland wants to merge 1 commit into
Conversation
_identify_bonds looped over every atom in Python and issued one KDTree.query call per atom, then built a list comprehension of cutoff distances for each. On an 80k atom receptor that is 80k round trips through scipy, and it accounted for about 70 percent of the time spent constructing a PDBQTReceptor. cKDTree.query accepts the whole coordinate array at once, so the search now happens in a single call with workers=-1, and the covalent radius lookup and cutoff comparison become array operations. Only the final dict assembly stays in Python. _identify_bonds is 9.6x faster at 80k atoms, and PDBQTReceptor construction is 3.0x faster end to end. Output is unchanged: verified identical to the previous implementation across random structures, non-contiguous index subsets, and the pdbqt files in test data. One behaviour change. A single atom input previously raised TypeError in receptor_pdbqt, because query with k=1 returns scalars that the slicing could not handle. It now returns no bonds, matching what molecule_pdbqt already did through its own early return. workers= requires scipy 1.6, so requirements.txt now pins that floor. Adds tests covering known geometries, non-contiguous indices, the degenerate sizes, and agreement with a brute force reference.
Collaborator
|
Very good, thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
_identify_bondsloops over every atom in Python and issues oneKDTree.querycall per atom, then builds a list comprehension of cutoff distances for each one. On an 80k atom receptor that is 80k round trips through scipy. ProfilingPDBQTReceptor(pdbqt_string)on an 80k atom input puts it at 1.738 s of a 2.441 s total, so roughly 70 percent of the time spent building a receptor is in this one function.cKDTree.queryalready accepts the whole coordinate array at once. This changes the function to make a single query for the entire structure withworkers=-1, and turns the radius lookup and cutoff comparison into array operations. Only the final dict assembly stays in Python:The function is duplicated verbatim in
molecule_pdbqt.pyandreceptor_pdbqt.py, so both copies get the same change and remain identical to each other. Deduplicating them is worth doing separately and is not attempted here.No linked issue.
Timing
_identify_bondson synthetic structures at protein-like density, same machine, best of a warmed run:End to end, the whole
PDBQTReceptor(pdbqt_string)call, which also includes parsing andget_atom_indices_by_residuethat this PR does not touch:Measured on Python 3.13.11, NumPy 2.5.1, SciPy 1.18.0, 8 cores. Batching alone accounts for most of it and
workers=-1contributes roughly another 3.8x on the query itself, so the gain will be smaller on a single core machine but the batching part holds regardless.Type of change
Checklist:
Please go through the following checklist before submitting the PR:
Additional Information
Output is unchanged. I checked the new implementation against a verbatim copy of the current one on 40 random structures spanning 2 to 400 atoms and varied density and composition, on 20 non-contiguous index subsets of the kind the annotation lists produce, and on all five pdbqt files in
test/rdkitmol_from_docking_data. Every bond dict came back identical.One behaviour change is worth flagging. A single atom input previously raised
TypeError: 'int' object is not subscriptableinreceptor_pdbqt, becausequerywithk=1returns scalars rather than arrays and the slicing could not handle them. It now returns no bonds.molecule_pdbqtalready returned no bonds for that case through its own early return, so this makes the two agree, but it does turn a crash into a quiet empty result.workers=was added in SciPy 1.6, released December 2020.requirements.txtpreviously pinned no versions at all, so this addsscipy>=1.6as the one bound this change actually needs, rather than trying to pin everything.Adds
test/identify_bonds_test.pywith 7 tests, run against both copies of the function: two and three atom geometries with known bonding, a methane-like case, non-contiguous indices, single atom and empty input, and agreement with a brute force reference over 20 random structures. Six of the seven also pass againstdevelop, which is the point, they characterise existing behaviour rather than the new implementation. The seventh is the single atom case described above. Full suite goes from 94 passed / 4 skipped to 101 passed / 4 skipped.