Source code for fedorov.space_group

# Copyright (c) 2019-2020 The Regents of the University of Michigan
# This file is part of the fedorov project, released under the BSD 3-Clause
# License.

import copy
import json
import os
import pickle
import warnings

import numpy as np
import rowan
import spglib as spg

from . import data, lattice, util


[docs]class PlaneGroup: """A class for plane group symmetry operation. This class provides method to initialize a crystal unit cell with plane group and Wyckoff possition information. :param plane_group_number: Plane group number between 1 and 17. :type plane_group_number: int """ plane_group_info_dir = os.path.join( data._DATA_PATH, "plane_group_info.pickle" ) with open(plane_group_info_dir, "rb") as f: plane_group_info_dict = pickle.load(f) plane_group_lattice_mapping_dir = os.path.join( data._DATA_PATH, "plane_group_lattice_mapping.json" ) with open(plane_group_lattice_mapping_dir, "r") as f: plane_group_lattice_mapping = json.load( f, object_hook=util.json_key_to_int ) def __init__(self, plane_group_number=1): if plane_group_number <= 0 or plane_group_number > 17: raise ValueError( "plane_group_number must be an integer between 1 and 17" ) self.plane_group_number = plane_group_number self.lattice_type = self.plane_group_lattice_mapping[ self.plane_group_number ] self.lattice = lattice.lattice_system_dict_2D[self.lattice_type] info = self.plane_group_info_dict[plane_group_number] self.translations = info["translations"] self.rotations = info["rotations"] def print_info(self): print( f"Plane group number: {self.plane_group_number}\n" f"lattice type: {self.lattice_type}\n" f"Default parameters for lattice: {self.lattice.lattice_params}" )
[docs] def get_basis_vectors( self, base_positions, base_type=[], base_quaternions=None, is_complete=False, apply_orientation=False, ): """Get the basis vectors for the defined crystall structure. :param base_positions: N by 2 np array of the Wyckoff postions :type base_positions: np.ndarray :param base_type: a list of string for particle type name :type base_type: list :param base_quaternions: N by 4 np array of quaternions, default None :type base_quaternions: np.ndarray :param is_complete: bool value to indicate if the positions are complete postions in a unitcell :type is_complete: bool :param apply_orientations: bool value to indicate if the space group symmetry should be applied to orientation :type apply_orientations: bool :return: basis_vectors :rtype: np.ndarray """ # check input accuracy if ( not isinstance(base_positions, np.ndarray) or len(base_positions.shape) == 1 or base_positions.shape[1] != 2 ): raise ValueError( "base_positions must be an numpy array of shape Nx3" ) if apply_orientation: if ( not isinstance(base_quaternions, np.ndarray) or len(base_quaternions.shape) == 1 or base_quaternions.shape[1] != 4 ): raise ValueError( "base_quaternions must be an numpy array of shape Nx4" ) if len(base_type): if ( not isinstance(base_type, list) or len(base_type) != base_positions.shape[0] or not all(isinstance(i, str) for i in base_type) ): raise ValueError( "base_type must contain a list of type name the same length" "as the number of basis positions" ) else: base_type = ["A"] * base_positions.shape[0] def _expand2d(matrix2d): matrix3d = np.identity(3) matrix3d[0:2, 0:2] = matrix2d return matrix3d threshold = 1e-6 reflection_exist = False for i in range(0, len(self.rotations)): # Generate the new set of positions from the base pos = data.wrap( self.rotations[i].dot(base_positions.T).T + self.translations[i] ) if apply_orientation: if np.linalg.det(self.rotations[i]) == 1: quat_rotate = rowan.from_matrix( _expand2d(self.rotations[i]), require_orthogonal=False ) quat = rowan.multiply(quat_rotate, base_quaternions) else: quat = base_quaternions reflection_exist = True if i == 0: positions = pos type_list = copy.deepcopy(base_type) if apply_orientation: quaternions = quat else: pos_comparisons = pos - positions[:, np.newaxis, :] norms = np.linalg.norm(pos_comparisons, axis=2) comps = np.all(norms > threshold, axis=0) positions = np.append(positions, pos[comps], axis=0) type_list += [x for x, y in zip(base_type, comps) if y] if apply_orientation: quaternions = np.append(quaternions, quat[comps], axis=0) if norms.min() < threshold: warnings.warn( "Orientation quaterions may have multiple values " "for the same particle postion under the symmetry " "operation for this space group and is not well " "defined, only the first occurance is used." ) if reflection_exist: warnings.warn( "Reflection operation is included in this space " "group, and is ignored for quaternion calculation." ) if is_complete and len(positions) != len(base_positions): raise ValueError( "the complete basis postions vector does not match the space " "group chosen. More positions are generated based on the " "symmetry operation within the provided space group" ) if apply_orientation: return data.wrap(positions), type_list, quaternions else: return data.wrap(positions), type_list
[docs] def get_lattice_vectors(self, **user_lattice_params): """Initialize the unitcell and return lattice vectors [a1, a2]. :param user_lattice_params: unit cell parameters, provide a, b, theta where applicable :type user_lattice_params: float :return: lattice_vectors :rtype: np.ndarray """ return self.lattice.get_lattice_vectors(**user_lattice_params)
[docs]class SpaceGroup: """A class for space group symmetry operation. This class provides method to initialize a crystal unit cell with space group and Wyckoff possition information. :param space_group_number: Space group number between 1 and 230. :type space_group_number: int """ space_group_hall_mapping_dir = os.path.join( data._DATA_PATH, "space_group_hall_mapping.json" ) with open(space_group_hall_mapping_dir, "r") as f: space_group_hall_mapping = json.load( f, object_hook=util.json_key_to_int ) space_group_lattice_mapping_dir = os.path.join( data._DATA_PATH, "space_group_lattice_mapping.json" ) with open(space_group_lattice_mapping_dir, "r") as f: space_group_lattice_mapping = json.load( f, object_hook=util.json_key_to_int ) def __init__(self, space_group_number=1): if space_group_number <= 0 or space_group_number > 230: raise ValueError( "space_group_number must be an integer between 1 and 230" ) self.space_group_number = space_group_number self.lattice_type = self.space_group_lattice_mapping[ self.space_group_number ] self.lattice = lattice.lattice_system_dict_3D[self.lattice_type] info = spg.get_symmetry_from_database( self.space_group_hall_mapping[space_group_number] ) self.translations = info["translations"] self.rotations = info["rotations"] def print_info(self): print( f"Space group number: {self.space_group_number}\n" f"lattice type: {self.lattice_type}\n" f"Default parameters for lattice: {self.lattice.lattice_params}" )
[docs] def get_basis_vectors( self, base_positions, base_type=[], base_quaternions=None, is_complete=False, apply_orientation=False, ): """Get the basis vectors for the defined crystall structure. :param base_positions: N by 3 np array of the Wyckoff postions :type base_positions: np.ndarray :param base_type: a list of string for particle type name :type base_type: list :param base_quaternions: N by 4 np array of quaternions, default None :type base_quaternions: np.ndarray :param is_complete: bool value to indicate if the positions are complete postions in a unitcell :type is_complete: bool :param apply_orientations: bool value to indicate if the space group symmetry should be applied to orientatioin :type apply_orientations: bool :return: basis_vectors :rtype: np.ndarray """ # check input accuracy if ( not isinstance(base_positions, np.ndarray) or len(base_positions.shape) == 1 or base_positions.shape[1] != 3 ): raise ValueError( "base_positions must be an numpy array of shape Nx3" ) if apply_orientation: if ( not isinstance(base_quaternions, np.ndarray) or len(base_quaternions.shape) == 1 or base_quaternions.shape[1] != 4 ): raise ValueError( "base_quaternions must be an numpy array of shape Nx4" ) if len(base_type): if ( not isinstance(base_type, list) or len(base_type) != base_positions.shape[0] or not all(isinstance(i, str) for i in base_type) ): raise ValueError( "base_type must contain a list of type name the same length" "as the number of basis positions" ) else: base_type = ["A"] * base_positions.shape[0] threshold = 1e-6 reflection_exist = False for i in range(0, len(self.rotations)): # Generate the new set of positions from the base pos = data.wrap( self.rotations[i].dot(base_positions.T).T + self.translations[i] ) if apply_orientation: if np.linalg.det(self.rotations[i]) == 1: quat_rotate = rowan.from_matrix( self.rotations[i], require_orthogonal=False ) quat = rowan.multiply(quat_rotate, base_quaternions) else: quat = base_quaternions reflection_exist = True if i == 0: positions = pos type_list = copy.deepcopy(base_type) if apply_orientation: quaternions = quat else: pos_comparisons = pos - positions[:, np.newaxis, :] norms = np.linalg.norm(pos_comparisons, axis=2) comps = np.all(norms > threshold, axis=0) positions = np.append(positions, pos[comps], axis=0) type_list += [x for x, y in zip(base_type, comps) if y] if apply_orientation: quaternions = np.append(quaternions, quat[comps], axis=0) if norms.min() < threshold: print( "Orientation quaterions may have multiple values " "for the same particle postion under the symmetry " "operation for this space group and is not well " "defined, only the first occurance is used." ) if reflection_exist: print( "Warning: reflection operation is included in this " "space group, and is ignored for quaternion calculation." ) if is_complete and len(positions) != len(base_positions): raise ValueError( "the complete basis postions vector does not match the space " "group chosen. More positions are generated based on the " "symmetry operation within the provided space group" ) if apply_orientation: return data.wrap(positions), type_list, quaternions else: return data.wrap(positions), type_list
[docs] def get_lattice_vectors(self, **user_lattice_params): """Initialize the unitcell and return lattice vectors [a1, a2, a3]. :param user_lattice_params: unit cell parameters, provide a, b, c, alpha, beta, gamma where applicable :type user_lattice_params: float :return: lattice_vectors :rtype: np.ndarray """ return self.lattice.get_lattice_vectors(**user_lattice_params)
[docs]class PointGroup: """A class to access all point group symmetry operations. This class provides method to access all point group symmetry operation in both rotational matrix form or quaternion form. :param point_group_number: Point group number between 1 and 32. :type point_group_number: int """ point_group_rotation_matrix_dir = os.path.join( data._DATA_PATH, "point_group_rotation_matrix_dict.pickle" ) with open(point_group_rotation_matrix_dir, "rb") as f: point_group_rotation_matrix_dict = pickle.load(f) point_group_quat_dir = os.path.join( data._DATA_PATH, "point_group_quat_dict.json" ) with open(point_group_quat_dir, "r") as f: point_group_quat_dict = json.load(f, object_hook=util.json_key_to_int) point_group_name_mapping_dir = os.path.join( data._DATA_PATH, "point_group_name_mapping.json" ) with open(point_group_name_mapping_dir, "r") as f: point_group_name_mapping = json.load( f, object_hook=util.json_key_to_int ) def __init__(self, point_group_number=1): if point_group_number <= 0 or point_group_number > 32: raise ValueError( "point_group_number must be an integer between 1 and 32" ) self.point_group_number = point_group_number self.point_group_name = self.point_group_name_mapping[ point_group_number ] self.rotation_matrix = self.point_group_rotation_matrix_dict[ point_group_number ]["rotations"] self.quaternion = self.point_group_quat_dict[point_group_number] def print_info(self): print( f"Point group number: {self.point_group_number}\n" f"Name {self.point_group_name}" )
[docs] def get_quaternion(self): """Get the quaternions for the point group symmetry. :return: list of quaternions :rtype: list """ return self.quaternion
[docs] def get_rotation_matrix(self): """Get the rotation matrixes for the point group symmetry. :return: n by 3 by 3 numpy array containing n rotational matrixes :rtype: numpy.ndarray """ return self.rotation_matrix