SANNpy implements the Solid Angle Nearest neighbors (SANN) algorithm for 2D and 3D point clouds. It provides:
- adaptive neighbor discovery driven by a KD-tree backend
- optional symmetrisation (additive or subtractive) of the neighbor list
- native support for periodic boundary conditions through box wrapping
- parallel execution via Numba for high performance on large datasets
The project exposes a single public function, sann, intended for scientific workflows that require robust local coordination analysis (e.g., soft matter, crystallography, point set topology).
Clone the repository and install the package in editable or standard mode:
pip install .
# or
pip install -e .SANNpy targets Python 3.10+ and depends on NumPy, SciPy, and Numba. Wheels are not yet published to PyPI; install from source as shown above.
import numpy as np
from SANNpy import sann
# Generate 3D particle coordinates
points = np.random.rand(1_000, 3)
# Compute neighbors with default settings
nnb, neighbors = sann(points)
print(nnb[:5]) # neighbor counts for the first few particles
print(neighbors[0]) # indices of neighbors for particle 0nnb, neighbors = sann(
points,
box=None,
m_init=None,
flavor="default",
symmetrize="none",
n_jobs=-1,
)- points (
np.ndarray, shape(n_points, dim)): Input coordinates (dim ∈ {2, 3}). - box (
array_like | None): Periodic box lengths. Supply a length-dimsequence to enable periodic wrapping. Points must lie within[0, box[i]). - m_init (
int | None): Initial neighbor guess. Defaults to 10 in 2D and 20 in 3D. Larger values may reduce re-querying for dense systems. - flavor (
"default" | "modified"): Variant of the SANN criterion. The modified flavour applies additional scaling that can assist in low-coordination environments. - symmetrize (
"none" | "additive" | "subtractive"): Post-process the neighbor list. Additive forms the union of directed edges; subtractive keeps only mutual links. - n_jobs (
int): Number of threads used by Numba.-1selects the environment default (typically all available cores).
Returns
- nnb (
np.ndarray): Number of neighbors assigned to each point. - neighbors (
list[np.ndarray]): Adjacency list containing integer indices for each point’s neighbors.
dim = 2 # or 3
points = np.random.rand(500, dim)
nnb, neighbors = sann(points)Default parameters suffice for most point sets.
box = np.array([1.0, 1.0])
nnb, neighbors = sann(points, box=box)Set box to enable periodic boundary conditions—useful for molecular simulations or lattice models. Ensure all coordinates are pre-wrapped into the box.
nnb, neighbors = sann(points, flavor="modified")The modified flavour introduces a number-of-neighbors dependent scaling that eliminates overcounting in low-coordination environments. See the accompanying paper for details.
nnb, neighbors = sann(points, symmetrize="additive")
# Example: build an undirected NetworkX graph
import networkx as nx
G = nx.Graph()
G.add_nodes_from(range(points.shape[0]))
for idx, row in enumerate(neighbors):
G.add_edges_from((idx, neighbor) for neighbor in row)Additive symmetrisation ensures the neighbor graph is bidirectional, suitable for undirected graph analyses. Note: This procedure does not guarantee a planar graph. See the accompanying paper to construct a planar graph/tiling.
nnb, neighbors = sann(points, symmetrize="subtractive")Use subtractive mode when you require mutual acknowledgement. Note: This procedure does not guarantee a planar graph. See the accompanying paper to construct a planar graph/tiling.
nnb, neighbors = sann(points, n_jobs=8)Set n_jobs to limit parallelism (useful on shared nodes). The default -1 delegates thread selection to Numba.
- For very dense systems, consider increasing
m_initto reduce the number of KD-tree re-queries. - If SANN fails to converge for some points, a runtime warning is raised and the remaining nodes receive all other points as neighbors.
- The exported surface is intentionally minimal (only
sann). Internal helpers may change without notice. - Tests and additional tooling are not bundled; integrate SANNpy within your existing scientific workflow or notebook environment.
If SANNpy contributes to your research, please consider citing the paper where this library is introdiced:
@misc{ulugöl2025solid,
title={Solid-angle based nearest-neighbor algorithm adapted for systems with low coordination number},
author={Alptuğ Ulugöl and Frank Smallenburg and Laura Filion},
year={2025},
eprint={2511.10748},
archivePrefix={arXiv},
primaryClass={cond-mat.soft},
url={https://arxiv.org/abs/2511.10748},
}The original 3D SANN is introduced in
@article{vanmeel2012a,
author = {van Meel, Jacobus A. and Filion, Laura and Valeriani, Chantal and Frenkel, Daan},
title = {A parameter-free, solid-angle based, nearest-neighbor algorithm},
journal = {The Journal of Chemical Physics},
volume = {136},
number = {23},
pages = {234107},
year = {2012},
month = {06},
issn = {0021-9606},
doi = {10.1063/1.4729313},
url = {https://doi.org/10.1063/1.4729313},
eprint = {https://pubs.aip.org/aip/jcp/article-pdf/doi/10.1063/1.4729313/15451929/234107_1_online.pdf},
}The 2D SANN is introduced in
@article{mugita2024simple,
author = {Mugita, Daigo and Souno, Kazuyoshi and Koyama, Hiroaki and Nakamura, Taisei and Isobe, Masaharu},
title = {Simple and efficient methods for local structural analysis in polydisperse hard disk systems},
journal = {The Journal of Chemical Physics},
volume = {160},
number = {17},
pages = {174104},
year = {2024},
month = {05},
issn = {0021-9606},
doi = {10.1063/5.0194873},
url = {https://doi.org/10.1063/5.0194873},
eprint = {https://pubs.aip.org/aip/jcp/article-pdf/doi/10.1063/5.0194873/19912852/174104_1_5.0194873.pdf},
}The modified SANN for low coordination systems is introduced in
@misc{ulugöl2025solid,
title={Solid-angle based nearest-neighbor algorithm adapted for systems with low coordination number},
author={Alptuğ Ulugöl and Frank Smallenburg and Laura Filion},
year={2025},
eprint={2511.10748},
archivePrefix={arXiv},
primaryClass={cond-mat.soft},
url={https://arxiv.org/abs/2511.10748},
}MIT