pyscal — python Structural Environment Calculator#
pyscal is a Python library for computing local atomic structural environments from atomistic simulation data. It provides fast C++-backed calculations of Steinhardt’s bond-orientational order parameters, common neighbor analysis, Voronoi tessellation, and many more descriptors — all through a clean functional API built on ASE (Atomic Simulation Environment).
Note
pyscal is distributed on PyPI as pyscal3 (the name pyscal on PyPI refers to an unrelated, now-unmaintained package). After pip install pyscal3, both import pyscal and import pyscal3 work and refer to the same library.
Key features#
ASE-first API — all functions take and return standard
ase.Atomsobjects. Results are stored inatoms.arraysandatoms.infowith thepyscal_prefix.Fast C++ core — neighbor finding, Steinhardt parameters, CNA, and other descriptors run in compiled C++ via pybind11.
Comprehensive descriptor suite:
Steinhardt bond-orientational order parameters and their averaged and disorder variants
Wigner \(W_l\) third-order rotational invariants
Minkowski structure metrics — Voronoi-area-weighted, parameter-free \(q_l\)
Voronoi tessellation for structural vector and Voronoi volume
Common neighbor analysis (CNA, adaptive CNA, diamond CNA)
Solid/liquid classification with clustering
Angular parameters and Ackland-Jones \(\chi\) parameters and classifier
Coordination measures — coordination number, effective coordination, and generalised coordination
Angular and bond-length distribution functions
Entropy parameter for structure distinction
Atomic deformation — strain, von Mises invariant, \(D^2_{\min}\), slip vector
Wigner-Seitz defect analysis — vacancies, interstitials, antisites
Atomic Cluster Expansion descriptors
Structure creation — built-in routines for common crystals, elements, general lattices, and grain boundaries.
Trajectory support — memory-efficient analysis of large LAMMPS dump trajectories.
Quick start#
from ase.build import bulk
import pyscal
# Create a structure
atoms = bulk("Cu", "fcc", cubic=True).repeat(3)
# Find neighbors
pyscal.find_neighbors(atoms, method="cutoff", cutoff=0)
# Calculate descriptors
q4, q6 = pyscal.steinhardt_parameter(atoms, l=[4, 6])
print(atoms.arrays["pyscal_q6"].mean()) # ≈ 0.57 for fcc
# Common neighbor analysis
result = pyscal.common_neighbor_analysis(atoms)
print(result) # {'fcc': 108, 'hcp': 0, 'bcc': 0, 'ico': 0, 'others': 0}
Results are stored directly on the ASE Atoms object:
Per-atom data →
atoms.arrays["pyscal_<name>"](NumPy arrays)System-level or ragged data →
atoms.info["pyscal_<name>"]
This makes it easy to combine pyscal with ASE’s I/O, visualisation, and analysis tools.
Why version 4?#
pyscal v4 is a major rewrite. The Python interface has been redesigned around ASE and is no longer source-compatible with v3. Working v3 code will need small changes to run on v4. It is therefore worth describing why this rewrite was needed and what it gives in return.
ASE Atoms is now the data structure#
pyscal no longer ships its own System or Atoms class. Every public function takes an ase.Atoms object as its first argument and writes results back to that same object — per-atom quantities into atoms.arrays["pyscal_*"] and global or ragged quantities into atoms.info["pyscal_*"]. Any structure that ASE can read or build can be analysed with pyscal directly, and the results travel naturally to the rest of the ecosystem (ASE I/O, pymatgen, OVITO, plotting libraries).
A functional API#
Every descriptor is a top-level function. There is no system state to keep in sync, no method-chaining order to remember, no class to subclass when adding a new descriptor. A typical session is
import pyscal
pyscal.find_neighbors(atoms, method="cutoff", cutoff=0)
q4, q6 = pyscal.steinhardt_parameter(atoms, l=[4, 6])
labels = pyscal.identify_ackland_jones(atoms)
Adding a new descriptor in v4 means writing a single function — not extending a class hierarchy.
A larger descriptor library#
In addition to everything that was in v3, this release adds Wigner \(W_l\) parameters, Minkowski structure metrics, Ackland-Jones structural classification, three coordination-number variants, angular and bond-length distribution functions, atomic deformation descriptors (strain tensor, von Mises invariant, \(D^2_{\min}\), slip vector), Wigner-Seitz defect analysis, and Atomic Cluster Expansion (ACE) descriptors up to body order four. Each is documented and benchmarked against published reference values where available.
What is the migration path?#
Code that used to read
from pyscal3 import System
sys = System('conf.dump')
sys.find.neighbors(method='cutoff', cutoff=3)
sys.calculate.steinhardt_parameter([4, 6])
sys.atoms.solid
now reads
import pyscal
from ase.io import read
atoms = read('conf.dump', format='lammps-dump-text')
pyscal.find_neighbors(atoms, method='cutoff', cutoff=3)
pyscal.steinhardt_parameter(atoms, l=[4, 6])
atoms.arrays['pyscal_solid']
If you need the v3 API, pin pyscal3<4.
Citing#
If you use pyscal in your work, please cite:
Sarath Menon, Grisell Díaz Leines and Jutta Rogal (2019). pyscal: A python module for structural analysis of atomic environments. Journal of Open Source Software, 4(43), 1824, https://doi.org/10.21105/joss.01824