crystal_space_group calculation style

Lucas M. Hale, lucas.hale@nist.gov, Materials Science and Engineering Division, NIST.

Introduction

The crystal_space_group calculation style characterizes a bulk atomic system (configuration) by determining its space group by evaluating symmetry elements of the box dimensions and atomic position. This is useful for analyzing relaxed systems to determine if they have transformed to a different crystal structure. An ideal unit cell based on the identified space group and the system’s box dimensions is also generated.

Version notes

  • 2018-07-09: Notebook added.

  • 2019-07-30: Function slightly updated

  • 2020-09-22: Setup and parameter definition streamlined. Method and theory expanded.

Additional dependencies

Disclaimers

  • NIST disclaimers

  • The results are sensitive to the symmetryprecision parameter as it defines the tolerance for identifying which atomic positions and box dimensions are symmetrically equivalent. A small symmetryprecision value may be able to differentiate between ideal and distorted crystals, but it will cause the calculation to fail if smaller than the variability in the associated system properties.

Method and Theory

The calculation relies on the spglib Python package, which itself is a wrapper around the spglib library. The library analyzes an atomic configuration to determine symmetry elements within a precision tolerance for the atomic positions and the box dimensions. It also contains a database of information related to the different space groups.

More information can be found at the spglib homepage.

Demonstration

1. Setup

1.1. Library imports

Import libraries needed by the calculation. The external libraries used are:

[1]:
# Standard library imports
from pathlib import Path
import os
import datetime
from copy import deepcopy

# http://www.numpy.org/
import numpy as np

# https://atztogo.github.io/spglib/python-spglib.html
import spglib

# https://github.com/usnistgov/atomman
import atomman as am
import atomman.lammps as lmp
import atomman.unitconvert as uc

# https://github.com/usnistgov/iprPy
import iprPy

print('Notebook last executed on', datetime.date.today(), 'using iprPy version', iprPy.__version__)
Notebook last executed on 2020-09-22 using iprPy version 0.10.2

1.2. Default calculation setup

[2]:
# Specify calculation style
calc_style = 'crystal_space_group'

# If workingdir is already set, then do nothing (already in correct folder)
try:
    workingdir = workingdir

# Change to workingdir if not already there
except:
    workingdir = Path('calculationfiles', calc_style)
    if not workingdir.is_dir():
        workingdir.mkdir(parents=True)
    os.chdir(workingdir)

2. Assign values for the calculation’s run parameters

2.1. Load initial unit cell system

  • ucell is an atomman.System representing a fundamental unit cell of the system (required). Here, this is generated using the load parameters and symbols.

[3]:
# Create ucell by loading prototype record
ucell = am.load('prototype', 'A1--Cu--fcc', symbols='Ni', a=3.52)

print(ucell)
avect =  [ 3.520,  0.000,  0.000]
bvect =  [ 0.000,  3.520,  0.000]
cvect =  [ 0.000,  0.000,  3.520]
origin = [ 0.000,  0.000,  0.000]
natoms = 4
natypes = 1
symbols = ('Ni',)
pbc = [ True  True  True]
per-atom properties = ['atype', 'pos']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   0.000   0.000   0.000
      1       1   0.000   1.760   1.760
      2       1   1.760   0.000   1.760
      3       1   1.760   1.760   0.000

2.5. Specify calculation-specific run parameters

  • symmetryprecision is a precision tolerance used for the atomic positions and box dimensions for determining symmetry elements. Default value is ‘0.01 angstrom’.

  • primitivecell is a boolean flag indicating if the returned unit cell is to be primitive (True) or conventional (False). Default value is False.

  • idealcell is a boolean flag indicating if the box dimensions and atomic positions are to be idealized based on the space group (True) or averaged based on their actual values (False). Default value is True.

[4]:
symmetryprecision = uc.set_in_units(0.01, 'angstrom')
primitivecell = True
idealcell = True

3. Define calculation function(s) and generate template LAMMPS script(s)

3.1. crystal_space_group()

[5]:
def crystal_space_group(system, symprec=1e-5, to_primitive=False,
                        no_idealize=False):
    """
    Uses spglib to evaluate space group information for a given system.

    Parameters
    ----------
    system : atomman.System
        The system to analyze.
    symprec : float
        Absolute length tolerance to use in identifying symmetry of atomic
        sites and system boundaries.
    to_primitive : bool
        Indicates if the returned unit cell is conventional (False) or
        primitive (True). Default value is False.
    no_idealize : bool
        Indicates if the atom positions in the returned unit cell are averaged
        (True) or idealized based on the structure (False).  Default value is
        False.

    Returns
    -------
    dict
        Results dictionary containing space group information and an associated
        unit cell system.
    """
    # Identify the standardized unit cell representation
    sym_data = spglib.get_symmetry_dataset(system.dump('spglib_cell'), symprec=symprec)
    ucell = spglib.standardize_cell(system.dump('spglib_cell'),
                                    to_primitive=to_primitive,
                                    no_idealize=no_idealize, symprec=symprec)

    # Convert back to atomman systems and normalize
    ucell = am.load('spglib_cell', ucell, symbols=system.symbols)
    ucell.atoms.pos -= ucell.atoms.pos[0]
    ucell = ucell.normalize()

    # Throw error if natoms > 2000
    natoms = ucell.natoms
    if natoms > 2000:
        raise RuntimeError('too many positions')

    # Average extra per-atom properties by mappings to primitive
    for index in np.unique(sym_data['mapping_to_primitive']):
        for key in system.atoms.prop():
            if key in ['atype', 'pos']:
                continue
            value = system.atoms.view[key][sym_data['mapping_to_primitive'] == index].mean()
            if key not in ucell.atoms.prop():
                ucell.atoms.view[key] = np.zeros_like(value)
            ucell.atoms.view[key][sym_data['std_mapping_to_primitive'] == index] = value

    # Get space group metadata
    sym_data = spglib.get_symmetry_dataset(ucell.dump('spglib_cell'))
    spg_type = spglib.get_spacegroup_type(sym_data['hall_number'])

    # Generate Pearson symbol
    if spg_type['number'] <= 2:
        crystalclass = 'a'
    elif spg_type['number'] <= 15:
        crystalclass = 'm'
    elif spg_type['number'] <= 74:
        crystalclass = 'o'
    elif spg_type['number'] <= 142:
        crystalclass = 't'
    elif spg_type['number'] <= 194:
        crystalclass = 'h'
    else:
        crystalclass = 'c'

    latticetype = spg_type['international'][0]
    if latticetype in ['A', 'B']:
        latticetype = 'C'

    pearson = crystalclass + latticetype + str(natoms)

    # Generate Wyckoff fingerprint
    fingerprint_dict = {}
    usites, uindices = np.unique(sym_data['equivalent_atoms'], return_index=True)
    for usite, uindex in zip(usites, uindices):
        atype = ucell.atoms.atype[uindex]
        wykoff = sym_data['wyckoffs'][uindex]
        if atype not in fingerprint_dict:
            fingerprint_dict[atype] = [wykoff]
        else:
            fingerprint_dict[atype].append(wykoff)
    fingerprint = []
    for atype in sorted(fingerprint_dict.keys()):
        fingerprint.append(''.join(sorted(fingerprint_dict[atype])))
    fingerprint = ' '.join(fingerprint)

    # Return results
    results_dict = spg_type
    results_dict['ucell'] = ucell
    results_dict['hall_number'] = sym_data['hall_number']
    results_dict['wyckoffs'] = sym_data['wyckoffs']
    results_dict['equivalent_atoms'] = sym_data['equivalent_atoms']
    results_dict['pearson'] = pearson
    results_dict['wyckoff_fingerprint'] = fingerprint

    return results_dict

4. Run calculation function(s)

[6]:
results_dict = crystal_space_group(ucell,
                                   symprec=symmetryprecision,
                                   to_primitive=primitivecell,
                                   no_idealize=not idealcell)

5. Report results

5.1. Display space group information

[7]:
for key in results_dict.keys():
    print(key)
    print(results_dict[key])
    print()
number
225

international_short
Fm-3m

international_full
F 4/m -3 2/m

international
F m -3 m

schoenflies
Oh^5

hall_symbol
-F 4 2 3

choice


pointgroup_schoenflies
m-3m

pointgroup_international
Oh

arithmetic_crystal_class_number
72

arithmetic_crystal_class_symbol
m-3mF

ucell
avect =  [ 2.489,  0.000,  0.000]
bvect =  [ 1.245,  2.156,  0.000]
cvect =  [ 1.245,  0.719,  2.032]
origin = [ 0.000,  0.000,  0.000]
natoms = 1
natypes = 1
symbols = ('Ni',)
pbc = [ True  True  True]
per-atom properties = ['atype', 'pos']
     id   atype  pos[0]  pos[1]  pos[2]
      0       1   0.000   0.000   0.000

hall_number
523

wyckoffs
['a']

equivalent_atoms
[0]

pearson
cF1

wyckoff_fingerprint
a