crystal_space_group - Methodology and code

Python imports

[1]:
# Standard library imports
from pathlib import Path
import shutil
import datetime
from copy import deepcopy
from math import floor
from typing import Optional, Tuple

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

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

# https://ipython.org/
from IPython.display import display, Markdown

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

# https://github.com/usnistgov/iprPy
import iprPy
from iprPy.tools import read_calc_file

print('Notebook last executed on', datetime.date.today(), 'using iprPy version', iprPy.__version__)
Notebook last executed on 2023-07-31 using iprPy version 0.11.6

1. Load calculation and view description

1.1. Load the calculation

[2]:
# Load the calculation being demoed
calculation = iprPy.load_calculation('crystal_space_group')

1.2. Display calculation description and theory

[3]:
# Display main docs and theory
display(Markdown(calculation.maindoc))
display(Markdown(calculation.theorydoc))

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.

  • 2022-03-11: Notebook updated to reflect version 0.11.

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.

2. Define calculation functions and generate files

This section defines the calculation functions and associated resource files exactly as they exist inside the iprPy package. This allows for the code used to be directly visible and modifiable by anyone looking to see how it works.

2.1. crystal_space_group()

This is the primary function for the calculation. The version of this function built in iprPy can be accessed by calling the calc() method of an object of the associated calculation class.

[4]:
# coding: utf-8

# Python script created by Lucas Hale

# 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

def crystal_space_group(system: am.System,
                        symprec: float = 1e-5,
                        to_primitive: bool = False,
                        no_idealize: bool = False) -> dict:
    """
    Uses spglib to evaluate space group information for a given system.

    Parameters
    ----------
    system : atomman.System
        The system to analyze.
    symprec : float, optional
        Absolute length tolerance to use in identifying symmetry of atomic
        sites and system boundaries. Default value is 1e-5
    to_primitive : bool, optional
        Indicates if the returned unit cell is conventional (False) or
        primitive (True). Default value is False.
    no_idealize : bool, optional
        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
        Dictionary of results consisting of keys:

        - **'number'** (*int*) The spacegroup number.
        - **'international_short'** (*str*) The short international spacegroup
          symbol.
        - **'international_full'** (*str*) The full international spacegroup
          symbol.
        - **'international'** (*str*) The international spacegroup symbol.
        - **'schoenflies'** (*str*) The schoenflies spacegroup symbol.
        - **'hall_symbol'** (*str*) The Hall symbol.
        - **'choice'** (*str*) The setting choice if there is one.
        - **'pointgroup_international'** (*str*) The international pointgroup
          symbol.
        - **'pointgroup_schoenflies'** (*str*) The schoenflies pointgroup
          symbol.
        - **'arithmetic_crystal_class_number'** (*int*) The arithmetic crystal
          class number.
        - **'arithmetic_crystal_class_symbol'** (*str*) The arithmetic crystal
          class symbol.
        - **'ucell'** (*am.System*) The spacegroup-processed unit cell.
        - **'hall_number'** (*int*) The Hall number.
        - **'wyckoffs'** (*list*) A list of the spacegroup's Wyckoff symbols
          where atoms are found.
        - **'equivalent_atoms'** (*list*) A list of indices indicating which
          atoms are equivalent to others.
        - **'pearson'** (*str*) The Pearson symbol.
        - **'wyckoff_fingerprint'** (*str*) The Wyckoff symbols joined
          together.
    """
    # 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

3. Specify input parameters

3.1. 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.

[5]:
# 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

3.2. Calculation-specific 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.

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

4. Run calculation and view results

4.1. Run calculation

All primary calculation method functions take a series of inputs and return a dictionary of outputs.

[7]:
results_dict = crystal_space_group(ucell,
                                   symprec=symmetryprecision,
                                   to_primitive=primitivecell,
                                   no_idealize=not idealcell)
print(results_dict.keys())
dict_keys(['number', 'international_short', 'international_full', 'international', 'schoenflies', 'hall_number', 'hall_symbol', 'choice', 'pointgroup_international', 'pointgroup_schoenflies', 'arithmetic_crystal_class_number', 'arithmetic_crystal_class_symbol', 'ucell', 'wyckoffs', 'equivalent_atoms', 'pearson', 'wyckoff_fingerprint'])

4.2. Report results

Values returned in the results_dict:

  • ‘number’ (int) The spacegroup number.

  • ‘international_short’ (str) The short international spacegroup symbol.

  • ‘international_full’ (str) The full international spacegroup symbol.

  • ‘international’ (str) The international spacegroup symbol.

  • ‘schoenflies’ (str) The schoenflies spacegroup symbol.

  • ‘hall_symbol’ (str) The Hall symbol.

  • ‘choice’ (str) The setting choice if there is one.

  • ‘pointgroup_international’ (str) The international pointgroup symbol.

  • ‘pointgroup_schoenflies’ (str) The schoenflies pointgroup symbol.

  • ‘arithmetic_crystal_class_number’ (int) The arithmetic crystal class number.

  • ‘arithmetic_crystal_class_symbol’ (str) The arithmetic crystal class symbol.

  • ‘ucell’ (am.System) The spacegroup-processed unit cell.

  • ‘hall_number’ (int) The Hall number.

  • ‘wyckoffs’ (list) A list of the spacegroup’s Wyckoff symbols where atoms are found.

  • ‘equivalent_atoms’ (list) A list of indices indicating which atoms are equivalent to others.

  • ‘pearson’ (str) The Pearson symbol.

  • ‘wyckoff_fingerprint’ (str) The Wyckoff symbols joined together.

[8]:
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_number
523

hall_symbol
-F 4 2 3

choice


pointgroup_international
m-3m

pointgroup_schoenflies
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

wyckoffs
['a']

equivalent_atoms
[0]

pearson
cF1

wyckoff_fingerprint
a