Source code for wulfric._spglib_interface

# ================================== LICENSE ===================================
# Wulfric - Cell, Atoms, K-path, visualization.
# Copyright (C) 2023-2025 Andrey Rybakov
#
# e-mail: anry@uv.es, web: adrybakov.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# ================================ END LICENSE =================================
import spglib
from copy import deepcopy
import numpy as np
from wulfric._syntactic_sugar import SyntacticSugar, add_sugar
from wulfric.crystal._crystal_validation import validate_atoms
from wulfric._exceptions import _raise_with_message, _SUPPORT_FOOTER
from wulfric.crystal._atoms import get_atom_species

from wulfric.constants._space_groups import CRYSTAL_FAMILY, CENTRING_TYPE

# Save local scope at this moment
old_dir = set(dir())
old_dir.add("old_dir")


[docs] def validate_spglib_data(cell, atoms, spglib_data) -> None: r""" Validate that ``cell`` and ``atoms["positions"]`` match the ones on which ``spglib_data`` was created. In details, it check that * ``cell`` is the same as ``spglib_data.original_cell`` * ``atoms["positions"]`` are the same as ``spglib_data.original_positions`` * ``wulfric.get_spglib_types(atoms=atoms)`` is the same as ``spglib_data.original_types``. Parameters ========== cell : (3, 3) |array-like|_ Matrix of a cell, rows are interpreted as vectors. In the language of |spglib|_ the same concept is usually called "basis vectors" or "lattice". atoms : dict Dictionary with N atoms. Expected keys: * "positions" : (N, 3) |array-like|_ Positions of the atoms in the basis of lattice vectors (``cell``). In other words - relative coordinates of atoms. * "names" : (N, ) list of str, optional * "species" : (N, ) list of str, optional * "spglib_types" : (N, ) list of int, optional spglib_data : dict A dictionary with the added syntactic sugar (i.e. with the dot access to the keys), that is produced via call to :py:func:`.get_spglib_data`. Raises ====== ValueError If ``cell`` and ``atoms`` do not match ``spglib_data``. """ if not np.allclose(cell, spglib_data.original_cell): raise ValueError( "Validation of spglib_data against cell and atoms: cell mismatch." ) if not np.allclose(atoms["positions"], spglib_data.original_positions): raise ValueError( "Validation of spglib_data against cell and atoms: atom's positions mismatch." ) if get_spglib_types(atoms=atoms) != spglib_data.original_types: raise ValueError( "Validation of spglib_data against cell and atoms: atom's types mismatch." )
[docs] def get_spglib_types(atoms): r""" Constructs spglib_types for the given atoms. First satisfied rule is applied 1. "spglib_types" in atoms Return ``atoms["spglib_types"]``. 2. "species" in atoms. ``spglib_types`` are deduced from ``atoms["species"]``. If two atoms have the same species, then they will have the same integer assigned to them in ``spglib_types``. 3. "names" in ``atoms`` Species are automatically deduced based on atom's names (via :py:func:`wulfric.crystal.get_atom_species`), and then the second rule is applied. Parameters ========== atoms : dict Dictionary with N atoms. At least one of the following keys is expected * "names" : (N, ) list of str, optional * "species" : (N, ) list of str, optional * "spglib_types" : (N, ) list of int, optional Returns ======= spglib_types : (N, ) list of int List of integer indices ready to be passed to |spglib|_. """ validate_atoms(atoms=atoms, raise_errors=True) if "spglib_types" in atoms: spglib_types = atoms["spglib_types"] else: if "species" not in atoms and "names" in atoms: species = [ get_atom_species(name=name, raise_on_fail=False) for name in atoms["names"] ] elif "species" in atoms: species = atoms["species"] else: raise ValueError( 'Expected at least one of "spglib_types", "species" or "names" keys in ""atoms, found none.' ) mapping = { name: index + 1 for index, name in enumerate(sorted(list(set(species)))) } spglib_types = [mapping[name] for name in species] return spglib_types
[docs] def get_spglib_data( cell, atoms, spglib_symprec=1e-5, spglib_angle_tolerance=-1, ): r""" Interface to |spglib|_. The idea is that this is the only way to access the data from |spglib|_. In that way one can associate a dataset with a given ``cell`` and ``atoms`` and re-use it when necessary. Parameters ========== cell : (3, 3) |array-like|_ Matrix of a cell, rows are interpreted as vectors. In the language of |spglib|_ the same concept is usually called "basis vectors" or "lattice". atoms : dict Dictionary with N atoms. Expected keys: * "positions" : (N, 3) |array-like|_ Positions of the atoms in the basis of lattice vectors (``cell``). In other words - relative coordinates of atoms. * "names" : (N, ) list of str, optional See Notes * "species" : (N, ) list of str, optional See Notes * "spglib_types" (N, ) list of int, optional See Notes .. hint:: Pass ``atoms = dict(positions=[[0, 0, 0]], spglib_types=[1])`` if you would like to interpret the ``cell`` alone (effectively assuming that the ``cell`` is a primitive one). spglib_symprec : float, default :math:`10^{-5}` Directly passed to |spglib|_. Tolerance parameter for the symmetry search. spglib_angle_tolerance : float, default -1 Directly passed to |spglib|_. Tolerance parameter for the symmetry search. Returns ======= spglib_data : dict A dictionary with the added syntactic sugar (i.e. with the dot access to the keys). Data that are included: * ``spglib_data.original_cell`` Same as the given ``cell`` * ``spglib_data.original_positions`` Same as the given ``atoms["positions"]`` * ``spglib_data.original_types`` Same as ``wulfric.get_spglib_types(atoms=atoms)`` for given ``atoms``. * ``spglib_data.space_group_number`` Number of the space group. ``1 <= spglib_data.space_group_number <= 230``. * ``spglib_data.crystal_family`` Crystal family. * "c" for cubic * "h" for hexagonal * "t" for tetragonal * "o" for orhorhombic * "m" for monoclinic * "a" for triclinic * ``spglib_data.centring_type`` Centring type. * "P" for primitive * "A" for side centered * "C" for side centered * "I" for body-centered * "R" for rhombohedral centring * "F" for all faces centered * ``spglib_data.conventional_cell`` Conventional cell associated with the given structure in the same spatial orientation. In other words, it is a choice of the cell for the same crystal. It can contain more than one lattice point. Same as ``std_lattice`` of |spglib-dataset|_ but rotated back with the ``std_rotation_matrix`` of |spglib-dataset|_. * ``spglib_data.conventional_positions`` N relative positions of the atoms in the basis of ``spglib_data.conventional_cell``. Same as ``std_positions`` of |spglib-dataset|_. * ``spglib_data.conventional_types`` N types of the atoms. Same as ``std_types`` of |spglib-dataset|_. * ``spglib_data.primitive_cell`` Primitive cell associated with the given structure in the same spatial orientation. In other words, it is a choice of the cell for the same crystal. It contains exactly one lattice point. Same as ``primitive_lattice`` returned by |spglib-find-primitive|_, but rotated back with the ``std_rotation_matrix`` of |spglib-dataset|_. * ``spglib_data.primitive_positions`` M relative positions of the atoms in the basis of ``spglib_data.primitive_cell``. Same as ``primitive_positions`` returned by |spglib-find-primitive|_. * ``spglib_data.primitive_types`` M types of the atoms. Same as ``primitive_types`` returned by |spglib-find-primitive|_. * ``spglib_data.symprec`` angle_tolerance Tolerance parameter that was used to call |spglib|_. * ``spglib_data.angle_tolerance`` Tolerance parameter that was used to call |spglib|_. Raises ====== ValueError If some input data are not what is expected. TypeError If some input data are not what is expected. RuntimeError If spglib fail to detect symmetry. Notes ===== |spglib|_ uses ``types`` to distinguish the atoms. To see how wulfric deduces ``types`` from given ``atoms`` see :py:func:`wulfric.get_spglib_types`. """ try: # Validate input data # # Validate that the atoms dictionary is what expected of it validate_atoms(atoms=atoms, required_keys=["positions"], raise_errors=True) # Validate cell TODO: write _cell_validation.py, # perhaps check if it can form a parallelepiped try: cell = np.array(cell, dtype=float) except Exception as e: _raise_with_message(e=e, message=f"cell is not array-like, got\n{cell}") if cell.shape != (3, 3): raise ValueError(f"Expected shape of (3, 3) for cell, got {cell.shape}.") # Just a dictionary with dot-like access to its keys spglib_data = SyntacticSugar() # Populate with the input data spglib_data.original_cell = deepcopy(cell) spglib_data.original_positions = deepcopy(atoms["positions"]) spglib_data.original_types = deepcopy(get_spglib_types(atoms=atoms)) spglib_data.symprec = spglib_symprec spglib_data.angle_tolerance = spglib_angle_tolerance dataset = spglib.get_symmetry_dataset( (cell, spglib_data.original_positions, spglib_data.original_types), symprec=spglib_symprec, angle_tolerance=spglib_angle_tolerance, ) if dataset is None: raise RuntimeError( f"spglib failed to detect symmetry for the given structure with spglib_symprec = {spglib_symprec} and spglib_angle_tolerance = {spglib_angle_tolerance}." ) # For spglib <= 2.4.0 if isinstance(dataset, dict): dataset = add_sugar(dataset) spglib_data.space_group_number = dataset.number spglib_data.crystal_family = CRYSTAL_FAMILY[dataset.number] spglib_data.centring_type = CENTRING_TYPE[dataset.number] # Rotate conventional cell back to the orientation of the given cell and atoms spglib_data.conventional_cell = ( dataset.std_lattice @ dataset.std_rotation_matrix ) spglib_data.conventional_positions = dataset.std_positions spglib_data.conventional_types = dataset.std_types primitive_cell, primitive_positions, primitive_types = spglib.find_primitive( (cell, spglib_data.original_positions, spglib_data.original_types), symprec=spglib_symprec, angle_tolerance=spglib_angle_tolerance, ) # Rotate primitive cell back to the orientation of the given cell and atoms spglib_data.primitive_cell = primitive_cell @ dataset.std_rotation_matrix spglib_data.primitive_positions = primitive_positions spglib_data.primitive_types = primitive_types return spglib_data except Exception as e: _raise_with_message( e=e, message=f"Call to spglib failed. Spglib version {spglib.__version__}." + _SUPPORT_FOOTER, )
# Populate __all__ with objects defined in this file __all__ = list(set(dir()) - old_dir) # Remove all semi-private objects __all__ = [i for i in __all__ if not i.startswith("_")] del old_dir