Introduction to structuretoolkit#
structuretoolkit is a Python library for building,
analysing and visualising atomistic structures for materials science. It does not introduce a
new structure class of its own – instead it operates directly on the
ase.atoms.Atoms object from the
Atomic Simulation Environment (ASE). You can therefore think of
structuretoolkit as a large collection of additional, ASE-compatible functions: anything you can
build with ase.build, you can analyse and post-process with structuretoolkit, and the result is
again a plain ase.atoms.Atoms object that you can keep using with ASE (writing files, running an ASE
calculator, etc.).
How the package is organised#
The functionality is grouped into four submodules:
structuretoolkit.build– construct new structures (compounds, surfaces, grain boundaries, SQS, meshes, …)structuretoolkit.analyse– analyse existing structures (neighbors, symmetry, strain, descriptors, …)structuretoolkit.common– small helper functions and converters to other representations (pymatgen, phonopy, pyscal)structuretoolkit.visualize– 3d visualisation of structures
All of these functions are also re-exported from the root of the package (e.g. structuretoolkit.get_neighbors)
for backwards compatibility, but it is recommended to import functions through their submodule
(structuretoolkit.analyse.get_neighbors) because that is where they are documented and grouped by
purpose. Throughout this notebook we therefore import structuretoolkit as stk and always call
functions as stk.analyse.xxx(), stk.build.xxx(), stk.common.xxx() or stk.visualize.xxx().
This notebook walks through (almost) every public function of structuretoolkit, grouped by submodule,
and shows how it is meant to be combined with ASE.
import warnings
warnings.filterwarnings(
"ignore"
) # silence third-party deprecation warnings for this demo
import numpy as np
from ase.build import bulk
import structuretoolkit as stk
print("structuretoolkit version:", stk.__version__)
structuretoolkit version: 0.0.46.dev25+g04a02d8ac.d20260626
structuretoolkit.common – helpers and converters#
The common submodule contains small utility functions that are used throughout the rest of the
package, plus converters that translate an ase.atoms.Atoms object into the structure representations
used by other materials-science packages (pymatgen, phonopy, pyscal). They are handy on their own
whenever you need a quick property of a structure without writing the boilerplate yourself.
Basic structure properties#
get_number_species_atoms() counts how many atoms of each chemical element are present, and
select_index() returns the indices of all atoms of a given element – both operate directly on the
Atoms object returned by ase.build.bulk().
structure = bulk("Fe", cubic=True).repeat(2) + bulk("Al", cubic=True).repeat((2, 1, 1))
print("Number of atoms per species:", stk.common.get_number_species_atoms(structure))
print(
"Indices of the Al atoms:",
stk.common.select_index(structure=structure, element="Al"),
)
Number of atoms per species: {'Al': 8, 'Fe': 16}
Indices of the Al atoms: [16 17 18 19 20 21 22 23]
Cell geometry#
get_cell()turns a float, a 3-vector, a 3x3 matrix or anAtomsobject into a normalised (3, 3) cell array - this is what most other functions use internally to accept flexible cell input.get_vertical_length()returns, for every cell vector, the height of the box measured perpendicular to the opposite face (the relevant length scale e.g. for slab thickness or periodic cutoffs).get_extended_positions()repeats the atoms across the periodic boundary so that a search for neighbors close to the cell boundary does not miss any periodic image.get_wrapped_coordinates()wraps arbitrary Cartesian coordinates back into the periodic cell.center_coordinates_in_unit_cell()re-wraps all atoms of a structure in place, e.g. after coordinates were shifted outside of[0, 1)in fractional units.
fe_bulk = bulk("Fe", cubic=True)
print("Normalised (3, 3) cell from a lattice constant:\n", stk.common.get_cell(2.87))
print(
"\nVertical heights of the Fe cell:",
stk.common.get_vertical_length(structure=fe_bulk),
)
extended_positions, original_indices = stk.common.get_extended_positions(
structure=fe_bulk, width=3.0, return_indices=True
)
print(
"\nAtoms after adding a 3 Angstrom buffer layer around the cell:",
len(extended_positions),
)
shifted = fe_bulk.copy()
shifted.positions += 5 * np.diag(shifted.cell) # move atoms several cells away
stk.common.center_coordinates_in_unit_cell(structure=shifted)
print("\nPositions after wrapping back into the cell:\n", shifted.positions)
Normalised (3, 3) cell from a lattice constant:
[[2.87 0. 0. ]
[0. 2.87 0. ]
[0. 0. 2.87]]
Vertical heights of the Fe cell: [2.87 2.87 2.87]
Atoms after adding a 3 Angstrom buffer layer around the cell: 91
Positions after wrapping back into the cell:
[[-6.68890263e-16 -6.68890263e-16 -6.68890263e-16]
[ 1.43500000e+00 1.43500000e+00 1.43500000e+00]]
Applying a homogeneous strain#
apply_strain() deforms the cell (and, by default, the atoms with it) according to a strain matrix
epsilon. With mode="linear" the deformation gradient is F = epsilon + 1; with mode="lagrangian"
epsilon is interpreted as the Lagrangian strain tensor (F^T F - 1) / 2, which must be symmetric.
Passing return_box=True leaves the original structure untouched and returns a strained copy instead.
structure = bulk("Fe", cubic=True)
strained = stk.common.apply_strain(structure=structure, epsilon=0.01, return_box=True)
print("Original volume: ", structure.get_volume())
print("Strained volume: ", strained.get_volume())
Original volume: 23.63990300000001
Strained volume: 24.356215700803006
Converting to other structure representations#
Several materials-science packages have their own structure classes. structuretoolkit provides
light-weight, round-trip converters so you never have to leave the ASE Atoms representation for long:
ase_to_pymatgen()/pymatgen_to_ase()/pymatgen_read_from_file()– convert to/frompymatgen.core.Structure, e.g. to usepymatgen’s symmetry or surface generation tools.ase_to_pyscal()– convert to apyscal3.core.System, which several of thestructuretoolkit.analysefunctions use internally for structural fingerprinting.atoms_to_phonopy()/phonopy_to_atoms()– convert to/fromphonopy.structure.atoms.PhonopyAtoms, e.g. as input for a phonon calculation withphonopy.
fe_bulk = bulk("Fe", cubic=True)
pmg_structure = stk.common.ase_to_pymatgen(structure=fe_bulk)
print(pmg_structure)
back_to_ase = stk.common.pymatgen_to_ase(structure=pmg_structure)
print("\nRound-tripped through pymatgen:", back_to_ase)
pyscal_system = stk.common.ase_to_pyscal(structure=fe_bulk)
print("\npyscal system:", type(pyscal_system))
phonopy_atoms = stk.common.atoms_to_phonopy(atom=fe_bulk)
print("\nphonopy atoms:", type(phonopy_atoms))
print(
"Round-tripped through phonopy:",
stk.common.phonopy_to_atoms(ph_atoms=phonopy_atoms),
)
Full Formula (Fe2)
Reduced Formula: Fe
abc : 2.870000 2.870000 2.870000
angles: 90.000000 90.000000 90.000000
pbc : True True True
Sites (2)
# SP a b c magmom
--- ---- --- --- --- --------
0 Fe 0 0 0 2.3
1 Fe 0.5 0.5 0.5 2.3
Round-tripped through pymatgen: Atoms(symbols='Fe2', pbc=True, cell=[2.87, 2.87, 2.87], initial_magmoms=...)
pyscal system: <class 'pyscal3.core.System'>
phonopy atoms: <class 'phonopy.structure.atoms.PhonopyAtoms'>
Round-tripped through phonopy: Atoms(symbols='Fe2', pbc=True, cell=[2.87, 2.87, 2.87])
Finally, structuretoolkit.common.SymmetryError is the exception raised by the symmetry-related
functions in structuretoolkit.analyse (see below) whenever spglib cannot determine the symmetry of a
structure, for example because the cell or the atomic positions are degenerate.
structuretoolkit.analyse – analysing existing structures#
This is the largest submodule. It contains everything needed to characterise the local and global geometry of a structure: neighbor lists, symmetry operations, strain, and a number of structural fingerprints that are commonly used to classify crystal structures or identify defects.
Neighbor lists: get_neighbors() and get_neighborhood()#
get_neighbors() is one of the most used functions in the package. It returns a Neighbors object
which lazily exposes distances, vecs (vectors), indices and shells (the indices of coordination
shells, found by clustering distances) for every atom of the structure, fully periodic-boundary-aware.
You can ask for a fixed number of neighbors (num_neighbors) and/or a cutoff_radius.
structure = bulk("Fe", cubic=True) # bcc iron
neigh = stk.analyse.get_neighbors(structure=structure, num_neighbors=8)
print("Distance to the 8 nearest neighbors of atom 0:\n", neigh.distances[0])
print("\nCoordination shell of each of those neighbors:\n", neigh.shells[0])
print("\nChemical symbols of the neighbors of atom 0:\n", neigh.chemical_symbols[0])
Distance to the 8 nearest neighbors of atom 0:
[2.48549291 2.48549291 2.48549291 2.48549291 2.48549291 2.48549291
2.48549291 2.48549291]
Coordination shell of each of those neighbors:
[1 1 1 1 1 1 1 1]
Chemical symbols of the neighbors of atom 0:
['Fe' 'Fe' 'Fe' 'Fe' 'Fe' 'Fe' 'Fe' 'Fe']
get_neighborhood() answers the same question for arbitrary points in space (not necessarily atomic
positions), which is useful to probe e.g. interstitial sites or grid points.
random_point_in_cell = np.dot(np.random.random(3), structure.cell)
neigh_point = stk.analyse.get_neighborhood(
structure=structure, positions=random_point_in_cell, num_neighbors=4
)
print(
"Distances from a random point in the cell to its 4 nearest atoms:\n",
neigh_point.distances,
)
Distances from a random point in the cell to its 4 nearest atoms:
[1.40751277 1.54226709 1.70976867 1.95412966]
Minimum-image distances: get_distances_array() and find_mic()#
get_distances_array() returns the full pairwise distance (or vector) matrix between two sets of
positions, automatically applying the minimum image convention for periodic directions.
find_mic() is the lower-level function that maps arbitrary displacement vectors onto their periodic
minimum image – it is what get_distances_array() and get_neighbors() use internally.
structure = bulk("Al", cubic=True)
distance_matrix = stk.analyse.get_distances_array(structure=structure)
print("Pairwise distance matrix:\n", distance_matrix)
displacement = structure.positions[0] - structure.positions[1] + 10 * structure.cell[0]
mic_distance = stk.analyse.find_mic(structure=structure, v=displacement, vectors=False)
print("\nDisplacement folded back via the minimum image convention:", mic_distance)
Pairwise distance matrix:
[[0. 2.86378246 2.86378246 2.86378246]
[2.86378246 0. 2.86378246 2.86378246]
[2.86378246 2.86378246 0. 2.86378246]
[2.86378246 2.86378246 2.86378246 0. ]]
Displacement folded back via the minimum image convention: 2.8637824638055176
Box symmetry#
get_symmetry() wraps spglib and returns a Symmetry object with
all rotation/translation pairs that leave the structure invariant. It is the basis for several
convenience functions:
get_spacegroup()– the international space-group symbol/number.get_symmetry_dataset()– the fullspglibsymmetry dataset (Wyckoff positions, point group, …).get_primitive_cell()– reduce a (super)cell to its primitive cell.symmetrize_vectors()/group_points_by_symmetry()/get_equivalent_points()– use the box symmetry to symmetrize per-atom vectors, or to find groups of geometrically equivalent points.get_ir_reciprocal_mesh()– the irreducible set of points of a reciprocal-space mesh (e.g. for a DFT k-point grid).
structure = bulk("Al", cubic=True).repeat(2)
sym = stk.analyse.get_symmetry(structure=structure)
print("Number of symmetry operations:", len(sym.rotations))
print("Space group:", stk.analyse.get_spacegroup(structure=structure))
primitive = stk.analyse.get_primitive_cell(structure=structure)
print(
"\nAtoms in the supercell vs. its primitive cell:",
len(structure),
"->",
len(primitive),
)
# Forces/displacements on a perfect crystal must symmetrize to (numerically) zero
random_vectors = np.random.random(structure.positions.shape)
print("\nNorm of a random vector field before/after symmetrization:")
print(
np.linalg.norm(random_vectors),
"->",
np.linalg.norm(stk.analyse.symmetrize_vectors(structure, random_vectors)),
)
mapping, grid_points = stk.analyse.get_ir_reciprocal_mesh(
structure=structure, mesh=[4, 4, 4]
)
print(
"\nIrreducible points of a 4x4x4 k-point mesh:",
len(np.unique(mapping)),
"out of",
len(grid_points),
)
Number of symmetry operations: 1536
Space group: {'InternationalTableSymbol': 'Fm-3m', 'Number': 225}
Atoms in the supercell vs. its primitive cell: 32 -> 1
Norm of a random vector field before/after symmetrization:
5.660616829549077 -> 8.161356608876601e-17
Irreducible points of a 4x4x4 k-point mesh: 10 out of 64
get_equivalent_atoms() answers a related, but independent, question: which atoms of the structure
map onto each other under the box symmetry. It is implemented via phonopy’s PhonopyAtoms and
spglib, rather than through the Symmetry class above.
structure = bulk("Al", cubic=True).repeat(2)
print("Equivalent-atom labels:", stk.analyse.get_equivalent_atoms(structure=structure))
Equivalent-atom labels: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Layers, tessellations and clustering#
get_layers()groups atoms into layers along the cell directions (or arbitraryplanes), which is useful for analysing slabs, stacking sequences or counting the number of atomic layers.get_voronoi_vertices(),get_voronoi_neighbors()andget_delaunay_neighbors()expose the Voronoi/Delaunay tessellation of the (periodic) atom positions.get_cluster_positions()merges positions that lie within a given distance of each other (DBSCAN clustering), e.g. to average out thermal noise or symmetric duplicates.
structure = bulk("Al", cubic=True).repeat(3)
layers = stk.analyse.get_layers(structure=structure)
print("Number of (111)-independent layers along x, y, z:", layers.max(axis=0) + 1)
voronoi_vertices = stk.analyse.get_voronoi_vertices(structure=bulk("Al", cubic=True))
print(
"\nVoronoi vertices of an fcc unit cell (octahedral + tetrahedral sites):",
len(voronoi_vertices),
)
voronoi_pairs = stk.analyse.get_voronoi_neighbors(structure=structure)
delaunay_pairs = stk.analyse.get_delaunay_neighbors(structure=structure)
print(
"Voronoi neighbor pairs:",
len(voronoi_pairs),
" Delaunay neighbor pairs:",
len(delaunay_pairs),
)
doubled_positions = np.append(structure.positions, structure.positions, axis=0)
clustered = stk.analyse.get_cluster_positions(
structure=structure, positions=doubled_positions
)
print(
"\nDuplicated positions clustered back down to:",
len(clustered),
"(original count:",
len(structure),
")",
)
Number of (111)-independent layers along x, y, z: [6 6 6]
Voronoi vertices of an fcc unit cell (octahedral + tetrahedral sites): 12
Voronoi neighbor pairs: 846 Delaunay neighbor pairs: 1051
Duplicated positions clustered back down to: 108 (original count: 108 )
get_average_of_unique_labels() is the small helper used internally by get_layers() and
get_cluster_positions(): given an array of (possibly repeated) integer labels, it returns the average
value of the elements that share a label.
labels = [0, 1, 0, 2]
values = [0, 1, 2, 3]
print(stk.analyse.get_average_of_unique_labels(labels=labels, values=values))
[1. 1. 3.]
Interstitial sites: get_interstitials()#
get_interstitials() returns an Interstitials object which automatically searches for interstitial
sites of a given coordination number, e.g. tetrahedral (num_neighbors=4) or octahedral
(num_neighbors=6) sites in a bcc lattice. Besides the positions, it exposes diagnostic quantities
like get_distances(), get_volumes() and get_steinhardt_parameters() of the candidate sites –
useful to judge how “clean” / symmetric the identified sites are.
bcc_fe = bulk("Fe", cubic=True)
octahedral = stk.analyse.get_interstitials(structure=bcc_fe, num_neighbors=6)
tetrahedral = stk.analyse.get_interstitials(structure=bcc_fe, num_neighbors=4)
print("Octahedral interstitial sites in bcc Fe:", len(octahedral.positions))
print("Tetrahedral interstitial sites in bcc Fe:", len(tetrahedral.positions))
print(
"\nDistance of the octahedral sites to their nearest neighbor atoms:",
octahedral.get_distances(),
)
Octahedral interstitial sites in bcc Fe: 6
Tetrahedral interstitial sites in bcc Fe: 12
Distance of the octahedral sites to their nearest neighbor atoms: [1.435 1.435 1.435 1.435 1.435 1.435]
Local strain: get_strain()#
get_strain() compares a (e.g. deformed, defective, or thermally displaced) structure against an ideal
reference structure of the same crystal type, and returns the Lagrangian strain tensor of every atom
based on its local neighborhood. This is a common way to visualise the strain field around a
dislocation, crack tip or grain boundary.
reference = bulk("Fe", cubic=True)
deformed = stk.common.apply_strain(structure=reference, epsilon=0.02, return_box=True)
strain = stk.analyse.get_strain(structure=deformed, ref_structure=reference)
print("Strain tensor shape:", strain.shape)
print("Per-atom isotropic strain (trace / 3):", np.trace(strain, axis1=1, axis2=2) / 3)
Strain tensor shape: (2, 3, 3)
Per-atom isotropic strain (trace / 3): [0.0202 0.0202]
Structural fingerprints (powered by pyscal)#
A group of functions builds on pyscal to classify the local crystal structure
around each atom – very useful to identify the crystal structure of an unknown configuration, or to
spot defects (grain boundaries, stacking faults, melted regions, …) in an otherwise crystalline
structure:
get_adaptive_cna_descriptors()– common neighbor analysis (fcc/hcp/bcc/icosahedral/other).get_centro_symmetry_descriptors()– centrosymmetry parameter (large for atoms near a defect).get_diamond_structure_descriptors()– identifies cubic/hexagonal diamond environments.get_steinhardt_parameters()– rotationally invariant bond-orientational order parameters.get_voronoi_volumes()– the Voronoi cell volume of every atom.find_solids()– counts how many atoms are “solid” vs. “liquid”-like, e.g. for melting-point calculations.
bcc_fe = bulk("Fe", cubic=True)
diamond_si = bulk("Si", crystalstructure="diamond", a=5.43, cubic=True)
print(
"Common neighbor analysis of bcc Fe:",
stk.analyse.get_adaptive_cna_descriptors(structure=bcc_fe),
)
print(
"Diamond-structure analysis of Si: ",
stk.analyse.get_diamond_structure_descriptors(structure=diamond_si),
)
centro = stk.analyse.get_centro_symmetry_descriptors(structure=bcc_fe, num_neighbors=8)
print("\nCentrosymmetry parameter of perfect bcc Fe (should be ~0):", centro)
q, cluster_id = stk.analyse.get_steinhardt_parameters(structure=bcc_fe.repeat(3))
print("\nSteinhardt parameters q4, q6 shape:", q.shape)
print(
"\nVoronoi cell volume per atom:", stk.analyse.get_voronoi_volumes(structure=bcc_fe)
)
print(
"Number of solid-like atoms (out of",
len(bcc_fe),
"):",
stk.analyse.find_solids(structure=bcc_fe),
)
Common neighbor analysis of bcc Fe: {'others': 0, 'fcc': 0, 'hcp': 0, 'bcc': 2, 'ico': 0}
Diamond-structure analysis of Si: {'others': 0, 'cubic diamond': 8, 'cubic diamond 1NN': 0, 'cubic diamond 2NN': 0, 'hex diamond': 0, 'hex diamond 1NN': 0, 'hex diamond 2NN': 0}
Centrosymmetry parameter of perfect bcc Fe (should be ~0): [2.36658272e-30 0.00000000e+00]
Steinhardt parameters q4, q6 shape: (2, 432)
Voronoi cell volume per atom: [11.8199515 11.8199515]
Number of solid-like atoms (out of 2 ): 2.0
Machine-learning structure descriptors#
Two further analysis tools generate per-atom descriptor vectors that are commonly used as input features for machine-learned interatomic potentials:
soap_descriptor_per_atom()computes the Smooth Overlap of Atomic Positions (SOAP) descriptor via the optionaldscribedependency (pip install structuretoolkit[dscribe]).get_snap_descriptors_per_atom(),get_snap_descriptor_derivatives()andget_snap_descriptor_names()compute the bispectrum components used by the SNAP machine-learning potential, via an embedded LAMMPS instance.
cu_fcc = bulk("Cu", "fcc", a=3.6, cubic=True)
soap = stk.analyse.soap_descriptor_per_atom(
structure=cu_fcc, r_cut=6.0, n_max=8, l_max=6
)
print("SOAP descriptor shape (n_atoms, n_features):", soap.shape)
SOAP descriptor shape (n_atoms, n_features): (4, 252)
ni_fcc = bulk("Ni", cubic=True)
snap_names = stk.analyse.get_snap_descriptor_names(twojmax=6)
print("Number of SNAP bispectrum components:", len(snap_names))
# The exact keyword arguments accepted by lammps.create_atoms() differ between LAMMPS
# python-module releases, so this call is most reliable when structuretoolkit and your
# LAMMPS installation are from a matching, recent release.
try:
snap_descriptors = stk.analyse.get_snap_descriptors_per_atom(
structure=ni_fcc, atom_types=["Ni"]
)
print("SNAP descriptor shape (n_atoms, n_components):", snap_descriptors.shape)
except TypeError as error:
print("Skipping execution - incompatible LAMMPS python module installed:", error)
Number of SNAP bispectrum components: 30
SNAP descriptor shape (n_atoms, n_components): (4, 30)
structuretoolkit.build – constructing new structures#
While ase.build already provides bulk crystals, surfaces and molecules, structuretoolkit.build adds
generators for several structure types that come up frequently in materials science but are not part of
ASE itself: ordered intermetallic compounds, high-index stepped/kinked surfaces, grain boundaries,
special quasirandom structures (SQS) for disordered alloys, regular real-space meshes, and a client for
the Materials Project database.
Ordered compound prototypes: B2(), C14(), C15(), C36(), D03()#
These functions build the unit cells of common binary intermetallic phases directly from two chemical
symbols (optionally a custom lattice constant), instead of having to look up Wyckoff positions and space
groups by hand. The result is, as always, a plain ase.atoms.Atoms object.
b2 = stk.build.B2("Fe", "Al") # CsCl-type FeAl
c15 = stk.build.C15("Mg", "Cu") # cubic Laves phase MgCu2
print("B2 FeAl: ", b2.get_chemical_formula(), "cell =", b2.cell.lengths())
print("C15 MgCu2:", c15.get_chemical_formula(), "cell =", c15.cell.lengths())
B2 FeAl: AlFe cell = [2.87 2.87 2.87]
C15 MgCu2: Cu16Mg8 cell = [7.38598547 7.38598547 7.38598547]
High-index stepped & kinked surfaces#
get_high_index_surface_info() derives the Miller index of a high-index surface that exposes a given
terrace, step and kink orientation (following the microfacet notation of Van Hove & Somorjai), and
high_index_surface() builds the corresponding relaxed slab (using pymatgen’s SpacegroupAnalyzer
internally to refine the structure).
miller_index, kink_dir, step_dir = stk.build.get_high_index_surface_info(
element="Ni",
crystal_structure="fcc",
lattice_constant=3.526,
terrace_orientation=[1, 1, 1],
step_orientation=[1, 1, 0],
kink_orientation=[1, 0, 1],
step_down_vector=[1, 1, 0],
length_step=2,
length_terrace=3,
length_kink=1,
)
print("High-index Miller index for this terrace/step/kink combination:", miller_index)
slab = stk.build.high_index_surface(
element="Ni",
crystal_structure="fcc",
lattice_constant=3.526,
terrace_orientation=[1, 1, 1],
step_orientation=[1, 1, 0],
kink_orientation=[1, 0, 1],
step_down_vector=[1, 1, 0],
length_step=2,
length_terrace=3,
length_kink=1,
layers=20,
vacuum=10,
)
print("Resulting stepped/kinked Ni slab:", slab)
High-index Miller index for this terrace/step/kink combination: [-9 -3 -5]
Resulting stepped/kinked Ni slab: Atoms(symbols='Ni20', pbc=True, cell=[[5.575095514876862, 0.0, 0.0], [-2.7875477574384036, 5.978623880124941, 0.0], [1.6071788640578362e-15, 2.522639151650182e-15, 26.247222712325165]])
Grain boundaries#
get_grainboundary_info() lists the geometrically possible grain boundaries (by CSL sigma value) for a
given rotation axis, using the optional aimsgb dependency
(pip install structuretoolkit[grainboundary]). Once you picked a sigma value and plane,
grainboundary() builds the actual bicrystal as an ase.atoms.Atoms object.
gb_options = stk.build.get_grainboundary_info(axis=[0, 0, 1], max_sigma=5)
print("Available grain boundaries for the [0 0 1] axis up to sigma=5:")
print(list(gb_options.keys()))
al_bulk = bulk("Al", cubic=True)
gb_structure = stk.build.grainboundary(
axis=[0, 0, 1], sigma=5, plane=[1, 2, 0], initial_struct=al_bulk
)
print(
"\nNumber of atoms in the sigma=5 [0 0 1](1 2 0) grain boundary:", len(gb_structure)
)
Available grain boundaries for the [0 0 1] axis up to sigma=5:
[np.int64(5)]
Number of atoms in the sigma=5 [0 0 1](1 2 0) grain boundary: 40
Regular real-space meshes: create_mesh()#
create_mesh() builds a regular grid of points spanning a cell (either an Atoms object or a raw cell)
– for example to sample a potential energy surface, or as the starting set of candidate positions in
a custom interstitial search.
structure = bulk("Al", cubic=True)
mesh = stk.create_mesh(cell=structure, n_mesh=4)
print("Mesh array shape (3 components, nx, ny, nz):", mesh.shape)
Mesh array shape (3 components, nx, ny, nz): (3, 4, 4, 4)
Special quasirandom structures (SQS)#
sqs_structures() generates Special Quasirandom Structures – supercells whose short-range order
parameters best approximate those of a fully random alloy with a given composition – via the
optional sqsgenerator dependency. This is the standard way to
set up a disordered-alloy supercell for DFT calculations. The function returns a SqsResultPack; use
.best() to get the single best result, which exposes both the resulting Atoms object (.atoms())
and its short-range order parameters (.sro()).
au_bulk = bulk("Au", cubic=True)
best_sqs = stk.build.sqs_structures(
structure=au_bulk,
composition={"Cu": 16, "Au": 16},
supercell=(2, 2, 2),
).best()
sqs_structure = best_sqs.atoms()
print("SQS composition:", stk.common.get_number_species_atoms(sqs_structure))
print(
"Short-range order parameter array shape (n_shells, n_species, n_species):",
best_sqs.sro().shape,
)
SQS composition: {'Cu': 16, 'Au': 16}
Short-range order parameter array shape (n_shells, n_species, n_species): (5, 2, 2)
Materials Project database access#
materialsproject_search() and materialsproject_by_id() wrap the
Materials Project REST API (via the optional mp-api dependency,
pip install structuretoolkit[mp-api]) and directly return ase.atoms.Atoms objects. Both functions
need a (free) Materials Project API key, either passed as api_key=... or exported as the MP_API_KEY
environment variable.
import os
if os.environ.get("MP_API_KEY"):
hits = list(stk.build.materialsproject_search("Fe", is_stable=True))
print("Stable Fe-containing entries found:", len(hits))
fe_ground_state = stk.build.materialsproject_by_id("mp-13")
print(fe_ground_state)
else:
print(
"Set the MP_API_KEY environment variable (or pass api_key=...) to query the Materials Project,"
" e.g.:\n\n"
" structures = list(stk.build.materialsproject_search('Fe', is_stable=True))\n"
" fe_ground_state = stk.build.materialsproject_by_id('mp-13')"
)
Set the MP_API_KEY environment variable (or pass api_key=...) to query the Materials Project, e.g.:
structures = list(stk.build.materialsproject_search('Fe', is_stable=True))
fe_ground_state = stk.build.materialsproject_by_id('mp-13')
structuretoolkit.visualize – looking at structures#
plot3d() renders any ase.atoms.Atoms object in 3d. The default mode="NGLview" produces an
interactive nglview widget – the nicest choice for
interactive work in a live Jupyter session, so it is what you will want to use day-to-day. For this
notebook we instead use mode="plotly" and render the resulting figure to a static PNG (which needs the
optional kaleido package, pip install kaleido), so the plot is also visible in a statically rendered
copy of this notebook, e.g. on GitHub.
structure = bulk("Al", cubic=True).repeat(2)
stk.visualize.plot3d(structure)
plot3d() accepts a scalar_field to color atoms by a per-atom property – this is a convenient way
to visualise the output of the analysis functions above. As an example, we color a small Al cell by the
centrosymmetry parameter after displacing one atom: the displaced atom (and, to a lesser extent, its
neighbors) lights up against the otherwise perfectly centrosymmetric lattice.
displaced = bulk("Al", cubic=True)
displaced.positions[0] += [0.4, 0.0, 0.0]
centro = stk.analyse.get_centro_symmetry_descriptors(
structure=displaced, num_neighbors=12
)
stk.visualize.plot3d(displaced, scalar_field=centro)
Where to go from here#
Browse the source code – every function has a docstring with the full list of arguments.
Have a look at the test suite, which contains many more focused, runnable usage examples for each function shown here.
structuretoolkitis also used as the structure-analysis backend ofpyiron_atomistics, where the same functions are available as methods directly on the structure object (e.g.structure.analyse.get_neighbors()).If a function is missing optional dependencies (
pyscal3,spglib,aimsgb,dscribe,mp-api,nglview, …), install the corresponding extra, e.g.pip install structuretoolkit[pyscal,symmetry].