Source code for aiida.orm.data.array.kpoints

# -*- coding: utf-8 -*-
"""
This module defines the classes related to band structures or dispersions
in a Brillouin zone, and how to operate on them.
"""

from aiida.orm.data.array import ArrayData
import numpy

__copyright__ = u"Copyright (c), This file is part of the AiiDA platform. For further information please visit http://www.aiida.net/. All rights reserved."
__license__ = "MIT license, see LICENSE.txt file."
__version__ = "0.7.0"
__authors__ = "The AiiDA team."


[docs]class KpointsData(ArrayData): """ Class to handle array of kpoints in the Brillouin zone. Provide methods to generate either user-defined k-points or path of k-points along symmetry lines. Internally, all k-points are defined in terms of crystal (fractional) coordinates. Cell and lattice vector coordinates are in Angstroms, reciprocal lattice vectors in Angstrom^-1 . :note: The methods setting and using the Bravais lattice info assume the PRIMITIVE unit cell is provided in input to the set_cell or set_cell_from_structure methods. """ def __init__(self, *args, **kwargs): super(KpointsData, self).__init__(*args, **kwargs) try: self._load_cell_properties() except AttributeError: pass @property def cell(self): """ The crystal unit cell. Rows are the crystal vectors in Angstroms. :return: a 3x3 numpy.array """ return numpy.array(self.get_attr('cell')) @cell.setter def cell(self, value): """ Set the crystal unit cell :param value: a 3x3 list/tuple/array of numbers (units = Angstroms). """ self._set_cell(value) def _set_cell(self, value): """ Validate if 'value' is a allowed crystal unit cell :param value: something compatible with a 3x3 tuple of floats """ from aiida.common.exceptions import ModificationNotAllowed from aiida.orm.data.structure import _get_valid_cell if self.is_stored: raise ModificationNotAllowed( "KpointsData cannot be modified, it has already been stored") the_cell = _get_valid_cell(value) self._set_attr('cell', the_cell) @property def pbc(self): """ The periodic boundary conditions along the vectors a1,a2,a3. :return: a tuple of three booleans, each one tells if there are periodic boundary conditions for the i-th real-space direction (i=1,2,3) """ # return copy.deepcopy(self._pbc) return ( self.get_attr('pbc1'), self.get_attr('pbc2'), self.get_attr('pbc3')) @pbc.setter def pbc(self, value): """ Set the value of pbc, i.e. a tuple of three booleans, indicating if the cell is periodic in the 1,2,3 crystal direction """ self._set_pbc(value) def _set_pbc(self, value): """ validate the pbc, then store them """ from aiida.common.exceptions import ModificationNotAllowed from aiida.orm.data.structure import get_valid_pbc if self.is_stored: raise ModificationNotAllowed( "The KpointsData object cannot be modified, it has already been stored") the_pbc = get_valid_pbc(value) self._set_attr('pbc1', the_pbc[0]) self._set_attr('pbc2', the_pbc[1]) self._set_attr('pbc3', the_pbc[2]) @property def labels(self): """ Labels associated with the list of kpoints. List of tuples with kpoint index and kpoint name: [(0,'G'),(13,'M'),...] """ label_numbers = self.get_attr('label_numbers', None) labels = self.get_attr('labels', None) if labels is None or label_numbers is None: return None return zip(label_numbers, labels) @labels.setter def labels(self, value): self._set_labels(value) def _set_labels(self, value): """ set label names. Must pass in input a list like: [[0,'X'],[34,'L'],... ] """ # check if kpoints were set try: self.get_kpoints() except AttributeError: raise AttributeError("Kpoints must be set before the labels") try: label_numbers = [int(i[0]) for i in value] except ValueError: raise ValueError("The input must contain an integer index, to map" " the labels into the kpoint list") labels = [str(i[1]) for i in value] if any([i > len(self.get_kpoints()) - 1 for i in label_numbers]): raise ValueError("Index of label exceeding the list of kpoints") self._set_attr('label_numbers', label_numbers) self._set_attr('labels', labels) # I commented this part, which atm I would leave it up to the user to set # new labels every time he modifies them. Anyway, labels should be set more # often by the set_path function # def append_label(self,value): # """ # Add a label to the existing ones. # """ # # get the list of existing labels # existing_labels = self.labels # # # validate the input # if value.__class__ is not list: # raise ValueError("Input must be a list of two values: index and label") # # if len(value)!=2: # raise ValueError("Input must be a list of length 2") # # try: # index = int(value[0]) # except ValueError: # raise ValueError("First item must be an integer kpoint index") # # # append # label = str(value[1]) # existing_labels.append([index,label]) # # # sort # new_labels= sorted(existing_labels, key=lambda x:x[0]) # # # overwrite the previous values # self.labels = new_labels # # def delete_label(self,index): # """ # Delete the label at position 'index'. Works as the python command # del list[index] # """ # index = int(index) # existing_labels = self.get_labels() # del existing_labels[index] # self.set_labels(existing_labels)
[docs] def set_cell_from_structure(self, structuredata): """ Set a cell to be used for symmetry analysis from an AiiDA structure. Inherits both the cell and the pbc's. To set manually a cell, use "set_cell" :param structuredata: an instance of StructureData """ from aiida.orm.data.structure import StructureData if not isinstance(structuredata, StructureData): raise ValueError("An instance of StructureData should be passed to " "the KpointsData, found instead {}" .format(structuredata.__class__)) cell = structuredata.cell self.set_cell(cell, structuredata.pbc)
[docs] def set_cell(self, cell, pbc=None): """ Set a cell to be used for symmetry analysis. To set a cell from an AiiDA structure, use "set_cell_from_structure". :param cell: 3x3 matrix of cell vectors. Orientation: each row represent a lattice vector. Units are Angstroms. :param pbc: list of 3 booleans, True if in the nth crystal direction the structure is periodic. Default = [True,True,True] """ self.cell = cell if pbc is None: pbc = [True, True, True] self.pbc = pbc self._load_cell_properties()
def _load_cell_properties(self): """ A function executed by the __init__ or by set_cell. If a cell is set, properties like a1, a2, a3, cosalpha, reciprocal_cell are set as well, although they are not stored in the DB. :note: units are Angstrom for the cell parameters, 1/Angstrom for the reciprocal cell parameters. """ # save a lot of variables that are used later, and just depend on the # cell the_cell = numpy.array(self.cell) reciprocal_cell = 2. * numpy.pi * numpy.linalg.inv(the_cell).transpose() self.reciprocal_cell = reciprocal_cell # units = 1/Angstrom self._a1 = numpy.array(the_cell[0, :]) # units = Angstrom self._a2 = numpy.array(the_cell[1, :]) # units = Angstrom self._a3 = numpy.array(the_cell[2, :]) # units = Angstrom self._a = numpy.linalg.norm(self._a1) # units = Angstrom self._b = numpy.linalg.norm(self._a2) # units = Angstrom self._c = numpy.linalg.norm(self._a3) # units = Angstrom self._b1 = reciprocal_cell[0, :] # units = 1/Angstrom self._b2 = reciprocal_cell[1, :] # units = 1/Angstrom self._b3 = reciprocal_cell[2, :] # units = 1/Angstrom self._cosalpha = numpy.dot(self._a2, self._a3) / self._b / self._c self._cosbeta = numpy.dot(self._a3, self._a1) / self._c / self._a self._cosgamma = numpy.dot(self._a1, self._a2) / self._a / self._b # Note: a,b,c,alpha,beta and gamma are referred to the input cell # not to the 'conventional' or rotated cell.
[docs] def set_kpoints_mesh(self, mesh, offset=[0., 0., 0.]): """ Set KpointsData to represent a uniformily spaced mesh of kpoints in the Brillouin zone. This excludes the possibility of set/get kpoints :param mesh: a list of three integers, representing the size of the kpoint mesh along b1,b2,b3. :param (optional) offset: a list of three floats between 0 and 1. [0.,0.,0.] is Gamma centered mesh [0.5,0.5,0.5] is half shifted [1.,1.,1.] by periodicity should be equivalent to [0.,0.,0.] Default = [0.,0.,0.]. """ from aiida.common.exceptions import ModificationNotAllowed # validate try: the_mesh = tuple(int(i) for i in mesh) if len(the_mesh) != 3: raise ValueError except (IndexError, ValueError, TypeError): raise ValueError("The kpoint mesh must be a list of three integers") try: the_offset = tuple(float(i) for i in offset) if len(the_offset) != 3: raise ValueError except (IndexError, ValueError, TypeError): raise ValueError("The offset must be a list of three floats") # check that there is no list of kpoints saved already # I cannot have both of them at the same time try: _ = self.get_array('kpoints') raise ModificationNotAllowed("KpointsData has already a kpoint-" "list stored") except KeyError: pass # store self._set_attr('mesh', the_mesh) self._set_attr('offset', the_offset)
[docs] def get_kpoints_mesh(self, print_list=False): """ Get the mesh of kpoints. :param print_list: default=False. If True, prints the mesh of kpoints as a list :raise AttributeError: if no mesh has been set :return mesh,offset: (if print_list=False) a list of 3 integers and a list of three floats 0<x<1, representing the mesh and the offset of kpoints :return kpoints: (if print_list = True) an explicit list of kpoints coordinates, similar to what returned by get_kpoints() """ mesh = self.get_attr('mesh') offset = self.get_attr('offset') if not print_list: return mesh, offset else: kpoints = numpy.mgrid[0:mesh[0], 0:mesh[1], 0:mesh[2]] kpoints = kpoints.reshape(3, -1).T offset_kpoints = kpoints + numpy.array(offset) offset_kpoints[:, 0] /= mesh[0] offset_kpoints[:, 1] /= mesh[1] offset_kpoints[:, 2] /= mesh[2] return offset_kpoints
[docs] def set_kpoints_mesh_from_density(self, distance, offset=[0., 0., 0.], force_parity=False): """ Set a kpoints mesh using a kpoints density, expressed as the maximum distance between adjacent points along a reciprocal axis :param distance: distance (in 1/Angstrom) between adjacent kpoints, i.e. the number of kpoints along each reciprocal axis i is :math:`|b_i|/distance` where :math:`|b_i|` is the norm of the reciprocal cell vector. :param (optional) offset: a list of three floats between 0 and 1. [0.,0.,0.] is Gamma centered mesh [0.5,0.5,0.5] is half shifted Default = [0.,0.,0.]. :param (optional) force_parity: if True, force each integer in the mesh to be even (except for the non-periodic directions). :note: a cell should be defined first. :note: the number of kpoints along non-periodic axes is always 1. """ try: rec_cell = self.reciprocal_cell except AttributeError: # rec_cell = numpy.eye(3) raise AttributeError("Cannot define a mesh from a density without " "having defined a cell") # I first round to the fifth digit |b|/distance (to avoid that e.g. # 3.00000001 becomes 4) kpointsmesh = [ max(int(numpy.ceil(round(numpy.linalg.norm(b) / distance, 5))), 1) if pbc else 1 for pbc, b in zip(self.pbc, rec_cell)] if force_parity: kpointsmesh = [k + (k % 2) if pbc else 1 for pbc, k in zip(self.pbc, kpointsmesh)] self.set_kpoints_mesh(kpointsmesh, offset=offset)
@property def _dimension(self): """ Dimensionality of the structure, found from its pbc (i.e. 1 if it's a 1D structure, 2 if its 2D, 3 if it's 3D ...). :return dimensionality: 0, 1, 2 or 3 :note: will return 3 if pbc has not been set beforehand """ try: return sum(self.pbc) except AttributeError: return 3 def _validate_kpoints_weights(self, kpoints, weights): """ Validate the list of kpoints and of weights before storage. Kpoints and weights must be convertible respectively to an array of N x dimension and N floats """ kpoints = numpy.array(kpoints) if len(kpoints) == 0: if self._dimension == 0: # replace empty list by Gamma point kpoints = numpy.array([[0., 0., 0.]]) else: raise ValueError( "empty kpoints list is valid only in zero dimension" "; instead here with have {} dimensions" "".format(self._dimension)) if len(kpoints.shape) <= 1: # list of scalars is accepted only in the 0D and 1D cases if self._dimension <= 1: # replace by singletons kpoints = kpoints.reshape(kpoints.shape[0], 1) else: raise ValueError("kpoints must be a list of lists in {}D case" "".format(self._dimension)) if kpoints.dtype != numpy.dtype(numpy.float): raise ValueError("kpoints must be an array of type floats. " "Found instead {}".format(kpoints.dtype)) if kpoints.shape[1] < self._dimension: raise ValueError("In a system which has {0} dimensions, kpoint need" "more than {0} coordinates (found instead {1})" .format(self._dimension, kpoints.shape[1])) if weights is not None: weights = numpy.array(weights) if weights.shape[0] != kpoints.shape[0]: raise ValueError("Found {} weights but {} kpoints" .format(weights.shape[0], kpoints.shape[0])) if weights.dtype != numpy.dtype(numpy.float): raise ValueError("weights must be an array of type floats. " "Found instead {}".format(weights.dtype)) return kpoints, weights
[docs] def set_kpoints(self, kpoints, cartesian=False, labels=None, weights=None, fill_values=0): """ Set the list of kpoints. If a mesh has already been stored, raise a ModificationNotAllowed :param kpoints: a list of kpoints, each kpoint being a list of one, two or three coordinates, depending on self.pbc: if structure is 1D (only one True in self.pbc) one allows singletons or scalars for each k-point, if it's 2D it can be a length-2 list, and in all cases it can be a length-3 list. Examples: * [[0.,0.,0.],[0.1,0.1,0.1],...] for 1D, 2D or 3D * [[0.,0.],[0.1,0.1,],...] for 1D or 2D * [[0.],[0.1],...] for 1D * [0., 0.1, ...] for 1D (list of scalars) For 0D (all pbc are False), the list can be any of the above or empty - then only Gamma point is set. The value of k for the non-periodic dimension(s) is set by fill_values :param cartesian: if True, the coordinates given in input are treated as in cartesian units. If False, the coordinates are crystal, i.e. in units of b1,b2,b3. Default = False :param labels: optional, the list of labels to be set for some of the kpoints. See labels for more info :param weights: optional, a list of floats with the weight associated to the kpoint list :param fill_values: scalar to be set to all non-periodic dimensions (indicated by False in self.pbc), or list of values for each of the non-periodic dimensions. """ from aiida.common.exceptions import ModificationNotAllowed # check that it is a 'dim'x #kpoints dimensional array the_kpoints, the_weights = self._validate_kpoints_weights(kpoints, weights) # if k-points have less than 3 coordinates (low dimensionality), fill # with constant values the non-periodic dimensions if the_kpoints.shape[1] < 3: if numpy.isscalar(fill_values): # replace scalar by a list of 3-the_kpoints.shape[1] identical # elements fill_values = [fill_values] * (3 - the_kpoints.shape[1]) if len(fill_values) < 3 - the_kpoints.shape[1]: raise ValueError("fill_values should be either a scalar or a " "length-{} list".format( 3 - the_kpoints.shape[1])) else: tmp_kpoints = numpy.zeros((the_kpoints.shape[0], 0)) i_kpts = 0 i_fill = 0 for idim in range(3): # check periodic boundary condition of each of the 3 dimensions: # - if it's a periodic one, fill with the k-points values # defined in input # - if it's non-periodic, fill with one of the values in # fill_values if self.pbc[idim]: tmp_kpoints = numpy.hstack( (tmp_kpoints, the_kpoints[:, i_kpts].reshape(( the_kpoints.shape[0], 1)))) i_kpts += 1 else: tmp_kpoints = numpy.hstack( (tmp_kpoints,numpy.ones( (the_kpoints.shape[0], 1) ) * fill_values[i_fill])) i_fill += 1 the_kpoints = tmp_kpoints # change reference and always store in crystal coords if cartesian: the_kpoints = self._change_reference(the_kpoints, to_cartesian=False) # check that we did not saved a mesh already if self.get_attr('mesh', None) is not None: raise ModificationNotAllowed( "KpointsData has already a mesh stored") # store self.set_array('kpoints', the_kpoints) if the_weights is not None: self.set_array('weights', the_weights) if labels is not None: self.labels = labels
[docs] def get_kpoints(self, also_weights=False, cartesian=False): """ Return the list of kpoints :param also_weights: if True, returns also the list of weights. Default = False :param cartesian: if True, returns points in cartesian coordinates, otherwise, returns in crystal coordinates. Default = False. """ try: kpoints = numpy.array(self.get_array('kpoints')) except KeyError: raise AttributeError("Before the get, first set a list of kpoints") # try: # if not all(self.pbc): # for i in range(3): # if not self.pbc[i]: # kpoints[:,i] = 0. # except AttributeError: # # no pbc data found -> assume (True,True,True) # pass # note that this operation may lead to duplicates if the kpoints were # set thinking that everything is 3D. # Atm, it's up to the user to avoid duplication, if he cares. # in the future, add the bravais_lattice for 2d and 1d cases, # and do a set() on the kpoints lists (before storing) if cartesian: kpoints = self._change_reference(kpoints, to_cartesian=True) if also_weights: try: the_weights = self.get_array('weights') except KeyError: raise AttributeError('No weights were set') weights = numpy.array(the_weights) return kpoints, weights else: return kpoints
def _change_reference(self, kpoints, to_cartesian=True): """ Change reference system, from cartesian to crystal coordinates (units of b1,b2,b3) or viceversa. :param kpoints: a list of (3) point coordinates :return kpoints: a list of (3) point coordinates in the new reference """ if not isinstance(kpoints, numpy.ndarray): raise ValueError("kpoints must be a numpy.array for method" "_change_reference()") try: rec_cell = self.reciprocal_cell except AttributeError: # rec_cell = numpy.eye(3) raise AttributeError( "Cannot use cartesian coordinates without having defined a cell") trec_cell = numpy.transpose(numpy.array(rec_cell)) if to_cartesian: matrix = trec_cell else: matrix = numpy.linalg.inv(trec_cell) # note: kpoints is a list Nx3, matrix is 3x3. # hence, first transpose kpoints, then multiply, finally transpose it back return numpy.transpose(numpy.dot(matrix, numpy.transpose(kpoints)))