Structure analysis#
calorine provides thin wrappers around spglib for common symmetry-analysis tasks on ASE Atoms objects: identifying the space group of a structure (get_spacegroup), reducing a structure to its primitive cell (get_primitive_structure), and labeling the Wyckoff site of each atom (get_wyckoff_sites).
These are useful, for example, when preparing a structure for a phonon or elastic-property calculation (which usually wants the primitive cell), or when generating or filtering training structures and you want to understand or deduplicate them by symmetry.
Setting up an example structure#
This notebook needs no external data, but we still set up a working directory for consistency with the other tutorials.
[1]:
import os
os.makedirs('structure_analysis', exist_ok=True)
os.chdir('structure_analysis')
[2]:
from ase.build import bulk
from ase.io import write
from calorine.tools import get_primitive_structure, get_spacegroup, get_wyckoff_sites
primitive = bulk('PbTe', crystalstructure='rocksalt', a=6.7)
supercell = primitive.repeat(2)
print(f'Primitive cell: {len(primitive)} atoms')
print(f'Supercell: {len(supercell)} atoms')
Primitive cell: 2 atoms
Supercell: 16 atoms
Identifying the space group#
get_spacegroup returns the space group in International Tables notation (or Schoenflies, via style='Schoenflies'). Repeating the primitive cell into a supercell does not change the space group, as expected.
[3]:
print('Primitive cell:', get_spacegroup(primitive))
print('Supercell: ', get_spacegroup(supercell))
Primitive cell: Fm-3m (225)
Supercell: Fm-3m (225)
The role of symprec#
Symmetry analysis is never exact for atomic coordinates that come from a simulation. Small numerical noise is unavoidable. symprec sets the distance tolerance (in Å) used when deciding whether two atoms are related by a symmetry operation. Below we displace the atoms in the supercell by a small random amount and compare a tight and a loose tolerance.
[4]:
rattled = supercell.copy()
rattled.rattle(stdev=0.005, seed=42)
print('Tight symprec (1e-5, default):', get_spacegroup(rattled, symprec=1e-5))
print('Loose symprec (0.05): ', get_spacegroup(rattled, symprec=0.05))
Tight symprec (1e-5, default): P1 (1)
Loose symprec (0.05): Fm-3m (225)
With the default (tight) tolerance, the random displacements are enough to make spglib report the trivial space group P1, even though the structure is still recognizably a rattled rock-salt lattice. Loosening symprec to a value comparable to the magnitude of the displacements recovers the correct space group. Choosing symprec is therefore a trade-off: too tight and thermal/numerical noise masks real symmetry; too loose and genuinely different structures may be identified as
symmetry-equivalent.
Reducing to the primitive cell#
get_primitive_structure uses the same kind of symmetry analysis to reduce a structure to its primitive cell. Applied to the rattled supercell with the same loose symprec used above, it correctly recovers the 2-atom primitive cell. The small displacements are absorbed into the symmetry tolerance rather than preventing the reduction.
[5]:
reduced = get_primitive_structure(rattled, symprec=0.05)
print(f'Rattled supercell: {len(rattled)} atoms,'
f' cell lengths {rattled.cell.lengths().round(3)} Å')
print(f'Reduced: {len(reduced)} atoms,'
f' cell lengths {reduced.cell.lengths().round(3)} Å')
Rattled supercell: 16 atoms, cell lengths [9.475 9.475 9.475] Å
Reduced: 2 atoms, cell lengths [4.738 4.738 4.738] Å
Labeling Wyckoff sites#
get_wyckoff_sites returns the Wyckoff symbol of every atom in the structure, in the same order as structure.numbers. In the rock-salt structure, Pb and Te each occupy one of the two distinct Wyckoff positions of space group \(Fm\bar{3}m\).
[6]:
wyckoff_primitive = get_wyckoff_sites(primitive)
for symbol, site in zip(primitive.symbols, wyckoff_primitive):
print(f'{symbol:3} : {site}')
Pb : 4a
Te : 4b
Applied to the (unrattled) supercell, every Pb atom maps to the same Wyckoff letter, and likewise for every Te atom. The label only depends on the local symmetry-equivalent site, not on which periodic image the atom happens to sit in.
[7]:
wyckoff_supercell = get_wyckoff_sites(supercell)
print(wyckoff_supercell)
['4a', '4b', '4a', '4b', '4a', '4b', '4a', '4b', '4a', '4b', '4a', '4b', '4a', '4b', '4a', '4b']
The labels can be attached to the structure as a per-atom array, which is then preserved when the structure is written to file (e.g. extended XYZ).
[8]:
primitive.new_array('wyckoff_sites', wyckoff_primitive, str)
write('PbTe-primitive.xyz', primitive)
print(open('PbTe-primitive.xyz').read())
2
Lattice="0.0 3.35 3.35 3.35 0.0 3.35 3.35 3.35 0.0" Properties=species:S:1:pos:R:3:wyckoff_sites:S:1 pbc="T T T"
Pb 0.00000000 0.00000000 0.00000000 4a
Te 3.35000000 0.00000000 0.00000000 4b