Performance improvements with KD-tree and grid-based algorithms#12
Merged
MayarMAhmed merged 15 commits intomainfrom Mar 11, 2026
Merged
Performance improvements with KD-tree and grid-based algorithms#12MayarMAhmed merged 15 commits intomainfrom
MayarMAhmed merged 15 commits intomainfrom
Conversation
…lgorithms The GridMesh class had several operations that materialized dense N×N distance matrices, causing excessive memory usage and slow runtimes (e.g., 20 GB for 50K points). This commit replaces them with spatial data structures and image-processing algorithms that operate in O(N) memory. Changes: - `TaskRemovePointsOutsideHull`: Replace per-point Python loop calling `hull.inside_hull()` with vectorized NumPy triangle test. For each hull triangle, all candidate points are tested simultaneously via a single matrix-vector dot product. Points classified as outside are excluded from subsequent triangle iterations (early exit). This was the single highest-impact change, reducing detection test time from ~180s to ~27s. - `filter_isolated_points_until_no_change`: Replace `sparse_distance_matrix().todense()` (which converted a sparse matrix to a full dense N×N matrix) with `KDTree.query_ball_point()` using `return_length=True`. - `separate_out_pockets`: Replace BFS on the dense distance matrix with `scipy.ndimage.label` on a 3D boolean grid. Points are mapped to integer (i,j,k) indices on the regular lattice, and connected- component labeling with 26-connectivity identifies pockets in O(N) time and memory. - `expand_around_existing_points`: Replace triple-nested Python for loop with `np.meshgrid` offset generation and NumPy broadcast addition. The `__keep_limited_points` and `__delete_limited_points` helper methods are now unused and are removed.
…ence `gen_points()` previously used `cdist(pts, pts_exclusion) < 1e-7` to find inclusion grid points that overlap with exclusion grid points. This materialized a full (N × M) float64 distance matrix to perform what is logically a set difference. Since both point sets are snapped to the same regular grid by `snap_points()`, coincident points have bit-identical coordinates. The new `remove_exclusion_points()` function exploits this by: 1. Reinterpreting each (x, y, z) row as a single `np.void` element via a zero-copy structured-array view (`_to_void`). 2. Using `np.isin()` for hash-based membership testing. Also extracted `remove_exclusion_points()` as a standalone function (previously inline in `gen_points()`) and added module and function docstrings.
…launay algorithms The per-frame `ConvexHull.volume()` method had three operations that materialized dense pairwise distance matrices or used pure-Python loops over all grid points. This commit replaces them with KD-tree queries and scipy's C-level Delaunay triangulation. Changes: - Contiguity neighbor counting: Replace `squareform(pdist(pts))` with `_count_neighbors_kdtree()`, a new helper that uses `KDTree.query_ball_point(..., return_length=True)` to count neighbors in O(N log N) time and O(N) memory. - Contiguous pocket flood fill: Replace the `while`-loop that called `cdist(pts, contig_pts)` on every iteration (materializing an N×M matrix that grew each iteration) with `flood_fill_contiguous_kdtree()`, a new function that builds a KD-tree once, pre-computes all neighbor lists in a single `query_ball_point` call, then runs BFS over the adjacency graph. - Convex hull point-in-hull test: Replace the pure-Python loop (`for pt in old_pts: outside_hull(pt, hull)`) with `scipy.spatial.Delaunay.find_simplex()`, which performs all point-in-hull tests in a single C-level call. Falls back to `_vectorized_hull_check()` (new helper using NumPy broadcast triangle tests) if Delaunay construction fails on degenerate geometry. New module-level helpers: - `_count_neighbors_kdtree()`: KD-tree neighbor counting. - `flood_fill_contiguous_kdtree()`: KD-tree BFS flood fill. - `_vectorized_hull_check()`: NumPy-vectorized triangle half-space test (Delaunay fallback).
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #12 +/- ##
==========================================
+ Coverage 77.31% 79.01% +1.69%
==========================================
Files 15 15
Lines 1133 1158 +25
Branches 157 162 +5
==========================================
+ Hits 876 915 +39
+ Misses 192 180 -12
+ Partials 65 63 -2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
MayarMAhmed
approved these changes
Mar 11, 2026
Member
MayarMAhmed
left a comment
There was a problem hiding this comment.
Looks good. I tested this against the CNR1 case (472 residues) that had been triggering the memory explosion previously, and it worked.
This was referenced Mar 11, 2026
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.
This PR replaces dense pairwise distance matrices throughout the POVME codebase with spatial data structures that operate in O(N log N) time and O(N) memory. The test suite runs 10.8× faster (291s → 27s), and peak memory usage for large systems drops substantially depending on the operation.
The changes preserve algorithmic correctness — the same points are kept/removed, the same volumes are computed, and the same pockets are identified. All existing tests pass with unchanged expected values.
Motivation
POVME's core operations (clash detection, neighbor counting, pocket separation, contiguity enforcement) all require answering the question "which points are close to which other points?" The previous implementation answered this by computing all pairwise distances via
cdist,pdist, orsparse_distance_matrix().todense(), producing N×N or N×M dense matrices. For typical production systems (50K grid points, 10K atoms), these matrices consumed massive amounts of RAM and dominated both runtime and memory.The key insight is that POVME only needs nearby pairs (within 2–3 Å), and on a regular grid, each point has at most 26 neighbors. Spatial data structures like KD-trees and integer-grid algorithms provide direct access to these sparse neighbor relationships without materializing the dense matrix.
Changes by file
hull.py— Per-frame volume calculationsquareform(pdist(pts))_count_neighbors_kdtree()usingKDTree.query_ball_point(return_length=True)cdist(pts, contig_pts)in while-loopflood_fill_contiguous_kdtree()using KD-tree BFSscipy.spatial.Delaunay.find_simplex()with_vectorized_hull_check()fallbackgridmesh.py— Pocket detection[pt for pt in pts if hull.inside_hull(pt)]scipy.spatial.KDTree+ Ray chunking +sparse_distance_matrixKDTree.query_ball_tree()callsparse_distance_matrix().todense()KDTree.query_ball_point(return_length=True)scipy.ndimage.labelon 3D boolean gridnp.meshgrid+ broadcast additionvolume.py— Point field generationcdist(pts, pts_exclusion) < 1e-7np.isinonnp.voidviewsregions.py— Region point generationcdist(result, center.reshape(1, -1))np.einsumvectorized normdetect.py— Pocket detection pipelinecdist(np.array([center]), cluster_pts)np.einsumvectorized normconfig/io.py— Pydantic configPydanticDeprecatedSince211warning: accessmodel_fieldson the class (self.__class__.model_fields) instead of the instance.Performance impact
Test suite runtime on my laptop.
(Super reproducible by the way /s)
hull.py+volume.py+regions.pychangesgridmesh.pyvectorized hull +KDTreechangesDesign decisions
Delaunay vs custom gift-wrapping hull
The volume path now uses
Delaunay.find_simplex()for convex hull tests, which is fully C-level and ~100× faster. A try/except block falls back to the vectorized triangle test, handling degenerate geometries where the Delaunay construction fails.scipy.ndimage.label for pocket separation
Since grid points lie on a regular lattice, they can be mapped to integer indices (i, j, k) and processed as a 3D image. Connected-component labeling with 26-connectivity replaces BFS for dense distance matrices.
Set difference for exclusion removal
Both inclusion and exclusion point sets are snapped to the same grid by
snap_points(), guaranteeing bit-identical coordinates for coincident points. This allowsnp.isinto be used on structured-array views instead ofcdist.