Finding Neighbors#

Neighbor finding is the foundation step for most pyscal descriptors. The find_neighbors function supports several methods:

Method

Description

"cutoff" with cutoff > 0

Fixed cutoff radius

"cutoff" with cutoff=0

Adaptive (per-atom) cutoff

"cutoff" with cutoff="sann"

SANN method (parameter-free)

"voronoi"

Voronoi tessellation

"number" with nmax=N

Nearest N neighbors

After calling find_neighbors, neighbor data is stored on the ASE Atoms object — typically in atoms.info (for ragged/variable-length lists).

import pyscal
from ase.build import bulk
import numpy as np

Fixed Cutoff#

The simplest approach: all atom pairs within a given distance are neighbors.

atoms = bulk("Cu", "fcc", cubic=True).repeat(4)

# Fixed cutoff of 3.0 Angstrom
pyscal.find_neighbors(atoms, method="cutoff", cutoff=3.0)

# Check that neighbor info was stored
print(f"Neighbor keys in atoms.info: {[k for k in atoms.info if 'neighbor' in k.lower()]}")
print(f"Neighbor method: {atoms.info.get('pyscal_neighbor_method', 'N/A')}")
Neighbor keys in atoms.info: ['pyscal_neighbors_found', 'pyscal_neighbor_method']
Neighbor method: cutoff

Adaptive Cutoff#

With cutoff=0, pyscal computes an adaptive cutoff for each atom based on the local structure.

atoms2 = bulk("Cu", "fcc", cubic=True).repeat(4)
pyscal.find_neighbors(atoms2, method="cutoff", cutoff=0)

print(f"Neighbors found: {atoms2.info.get('pyscal_neighbors_found', False)}")
print(f"Method: {atoms2.info.get('pyscal_neighbor_method', 'N/A')}")
Neighbors found: True
Method: cutoff

SANN Method#

The Solid-Angle based Nearest Neighbor (SANN) method determines neighbors based on solid angles, requiring no input parameters.

atoms3 = bulk("Cu", "fcc", cubic=True).repeat(4)
pyscal.find_neighbors(atoms3, method="cutoff", cutoff="sann")

print(f"SANN neighbors found: {atoms3.info.get('pyscal_neighbors_found', False)}")
SANN neighbors found: True

Voronoi Tessellation#

Voronoi tessellation determines neighbors by shared Voronoi faces.

atoms4 = bulk("Cu", "fcc", cubic=True).repeat(4)
pyscal.find_neighbors(atoms4, method="voronoi")

# Voronoi also computes volumes
if "pyscal_voronoi_volume" in atoms4.arrays:
    print(f"Voronoi volume (atom 0): {atoms4.arrays['pyscal_voronoi_volume'][0]:.4f}")
Voronoi volume (atom 0): 11.7615

Nearest N Neighbors#

Find exactly N nearest neighbors for each atom.

atoms5 = bulk("Cu", "fcc", cubic=True).repeat(4)
pyscal.find_neighbors(atoms5, method="number", nmax=12)

print(f"Nearest-12 neighbors found: {atoms5.info.get('pyscal_neighbors_found', False)}")
Nearest-12 neighbors found: True

PBC-Aware Distance#

get_distance computes the minimum-image distance between two positions, respecting periodic boundary conditions.

atoms6 = bulk("Cu", "fcc", cubic=True)
pos = atoms6.get_positions()

# Distance between first two atoms
d = pyscal.get_distance(atoms6, pos[0], pos[1])
print(f"Distance between atom 0 and atom 1: {d:.4f} Angstrom")
Distance between atom 0 and atom 1: 2.5527 Angstrom

Using Neighbors for Descriptors#

Once neighbors are found, you can compute any descriptor. Here’s a quick example:

atoms = bulk("Cu", "fcc", cubic=True).repeat(4)
pyscal.find_neighbors(atoms, method="cutoff", cutoff=0)

# Now compute Steinhardt parameters
q = pyscal.steinhardt_parameter(atoms, l=[4, 6])
print(f"q4 mean: {q[0].mean():.4f}")
print(f"q6 mean: {q[1].mean():.4f}")
q4 mean: 0.1909
q6 mean: 0.5745

Which Method Should I Use?#

Method

Best for

Adaptive (cutoff=0)

General-purpose, good default

Fixed cutoff

When you know the exact cutoff radius

SANN

Parameter-free, works well for disordered systems

Voronoi

When you also need Voronoi volumes/vectors

Nearest N

When you need exactly N neighbors (e.g., 12 for FCC)