Source code for aiida.orm.nodes.data.array.bands

# -*- coding: utf-8 -*-
###########################################################################
# This file is part of the AiiDA code.                                    #
#                                                                         #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida-core #
# For further information on the license, see the LICENSE.txt file        #
# For further information please visit http://www.aiida.net               #
###########################################################################
"""
This module defines the classes related to band structures or dispersions
in a Brillouin zone, and how to operate on them.
"""
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import io
from string import Template

import six
from six.moves import range, zip
import numpy

from aiida.common.exceptions import ValidationError
from aiida.common.utils import prettify_labels, join_labels
from .kpoints import KpointsData

[docs]def prepare_header_comment(uuid, plot_info, comment_char='#'): from aiida import get_file_header filetext = [] filetext += get_file_header(comment_char='').splitlines() filetext.append('') filetext.append('Dumped from BandsData UUID={}'.format(uuid)) filetext.append('\tpoints\tbands') filetext.append('\t{}\t{}'.format(*plot_info['y'].shape)) filetext.append('') filetext.append('\tlabel\tpoint') for l in plot_info['raw_labels']: filetext.append('\t{}\t{:.8f}'.format(l, l)) return '\n'.join('{} {}'.format(comment_char, l) for l in filetext)
# TODO: set and get bands could have more functionalities: how do I know the number of bands for example?
[docs]def find_bandgap(bandsdata, number_electrons=None, fermi_energy=None): """ Tries to guess whether the bandsdata represent an insulator. This method is meant to be used only for electronic bands (not phonons) By default, it will try to use the occupations to guess the number of electrons and find the Fermi Energy, otherwise, it can be provided explicitely. Also, there is an implicit assumption that the kpoints grid is "sufficiently" dense, so that the bandsdata are not missing the intersection between valence and conduction band if present. Use this function with care! :param number_electrons: (optional, float) number of electrons in the unit cell :param fermi_energy: (optional, float) value of the fermi energy. :note: By default, the algorithm uses the occupations array to guess the number of electrons and the occupied bands. This is to be used with care, because the occupations could be smeared so at a non-zero temperature, with the unwanted effect that the conduction bands might be occupied in an insulator. Prefer to pass the number_of_electrons explicitly :note: Only one between number_electrons and fermi_energy can be specified at the same time. :return: (is_insulator, gap), where is_insulator is a boolean, and gap a float. The gap is None in case of a metal, zero when the homo is equal to the lumo (e.g. in semi-metals). """ def nint(num): """ Stable rounding function """ if (num > 0): return int(num + .5) else: return int(num - .5) if fermi_energy and number_electrons: raise ValueError('Specify either the number of electrons or the Fermi energy, but not both') try: stored_bands = bandsdata.get_bands() except KeyError: raise KeyError('Cannot do much of a band analysis without bands') if len(stored_bands.shape) == 3: # I write the algorithm for the generic case of having both the # spin up and spin down array # put all spins on one band per kpoint bands = numpy.concatenate([_ for _ in stored_bands], axis=1) else: bands = stored_bands # analysis on occupations: if fermi_energy is None: num_kpoints = len(bands) if number_electrons is None: try: _, stored_occupations = bandsdata.get_bands(also_occupations=True) except KeyError: raise KeyError("Cannot determine metallicity if I don't have either fermi energy, or occupations") # put the occupations in the same order of bands, also in case of multiple bands if len(stored_occupations.shape) == 3: # I write the algorithm for the generic case of having both the # spin up and spin down array # put all spins on one band per kpoint occupations = numpy.concatenate([_ for _ in stored_occupations], axis=1) else: occupations = stored_occupations # now sort the bands by energy # Note: I am sort of assuming that I have an electronic ground state # sort the bands by energy, and reorder the occupations accordingly # since after joining the two spins, I might have unsorted stuff bands, occupations = [ numpy.array(y) for y in zip(*[ list(zip(*j)) for j in [sorted(zip(i.tolist(), i.tolist()), key=lambda x: x) for i in zip(bands, occupations)] ]) ] number_electrons = int(round(sum([sum(i) for i in occupations]) / num_kpoints)) homo_indexes = [numpy.where(numpy.array([nint(_) for _ in x]) > 0)[-1] for x in occupations] if len(set(homo_indexes)) > 1: # there must be intersections of valence and conduction bands return False, None else: homo = [_[_] for _ in zip(bands, homo_indexes)] try: lumo = [_[_ + 1] for _ in zip(bands, homo_indexes)] except IndexError: raise ValueError('To understand if it is a metal or insulator, ' 'need more bands than n_band=number_electrons') else: bands = numpy.sort(bands) number_electrons = int(number_electrons) # find the zero-temperature occupation per band (1 for spin-polarized # calculation, 2 otherwise) number_electrons_per_band = 4 - len(stored_bands.shape) # 1 or 2 # gather the energies of the homo band, for every kpoint homo = [i[number_electrons // number_electrons_per_band - 1] for i in bands] # take the nth level try: # gather the energies of the lumo band, for every kpoint lumo = [i[number_electrons // number_electrons_per_band] for i in bands] # take the n+1th level except IndexError: raise ValueError('To understand if it is a metal or insulator, ' 'need more bands than n_band=number_electrons') if number_electrons % 2 == 1 and len(stored_bands.shape) == 2: # if #electrons is odd and we have a non spin polarized calculation # it must be a metal and I don't need further checks return False, None # if the nth band crosses the (n+1)th, it is an insulator gap = min(lumo) - max(homo) if gap == 0.: return False, 0. elif gap < 0.: return False, None else: return True, gap # analysis on the fermi energy else: # reorganize the bands, rather than per kpoint, per energy level # I need the bands sorted by energy bands.sort() levels = bands.transpose() max_mins = [(max(i), min(i)) for i in levels] if fermi_energy > bands.max(): raise ValueError("The Fermi energy is above all band energies, don't know what to do") if fermi_energy < bands.min(): raise ValueError("The Fermi energy is below all band energies, don't know what to do.") # one band is crossed by the fermi energy if any(i < fermi_energy and fermi_energy < i for i in max_mins): return False, None # case of semimetals, fermi energy at the crossing of two bands # this will only work if the dirac point is computed! elif (any(i == fermi_energy for i in max_mins) and any(i == fermi_energy for i in max_mins)): return False, 0. # insulating case else: # take the max of the band maxima below the fermi energy homo = max([i for i in max_mins if i < fermi_energy]) # take the min of the band minima above the fermi energy lumo = min([i for i in max_mins if i > fermi_energy]) gap = lumo - homo if gap <= 0.: raise Exception('Something wrong has been implemented. Revise the code!') return True, gap
[docs]class BandsData(KpointsData): """ Class to handle bands data """ # Associate file extensions to default plotting formats _custom_export_format_replacements = { 'dat': 'dat_multicolumn', 'png': 'mpl_png', 'pdf': 'mpl_pdf', 'py': 'mpl_singlefile', 'gnu': 'gnuplot' }
[docs] def set_kpointsdata(self, kpointsdata): """ Load the kpoints from a kpoint object. :param kpointsdata: an instance of KpointsData class """ if not isinstance(kpointsdata, KpointsData): raise ValueError('kpointsdata must be of the KpointsData class') try: self.cell = kpointsdata.cell except AttributeError: pass try: self.pbc = kpointsdata.pbc except AttributeError: pass try: the_kpoints = kpointsdata.get_kpoints() except AttributeError: the_kpoints = None try: the_weights = kpointsdata.get_kpoints(also_weights=True) except AttributeError: the_weights = None self.set_kpoints(the_kpoints, weights=the_weights) try: self.labels = kpointsdata.labels except (AttributeError, TypeError): self.labels = []
[docs] def _validate_bands_occupations(self, bands, occupations=None, labels=None): """ Validate the list of bands and of occupations before storage. Kpoints must be set in advance. Bands and occupations must be convertible into arrays of Nkpoints x Nbands floats or Nspins x Nkpoints x Nbands; Nkpoints must correspond to the number of kpoints. """ try: kpoints = self.get_kpoints() except AttributeError: raise AttributeError('Must first set the kpoints, then the bands') the_bands = numpy.array(bands) if len(the_bands.shape) not in [2, 3]: raise ValueError('Bands must be an array of dimension 2' '([N_kpoints, N_bands]) or of dimension 3 ' ' ([N_arrays, N_kpoints, N_bands]), found instead {}'.format(len(the_bands.shape))) list_of_arrays_to_be_checked = [] # check that the shape of everything is consistent with the kpoints num_kpoints_from_bands = the_bands.shape if len(the_bands.shape) == 2 else the_bands.shape if num_kpoints_from_bands != len(kpoints): raise ValueError('There must be energy values for every kpoint') if occupations is not None: the_occupations = numpy.array(occupations) if the_occupations.shape != the_bands.shape: raise ValueError('Shape of occupations {} different from shape' 'shape of bands {}'.format(the_occupations.shape, the_bands.shape)) if not the_bands.dtype.type == numpy.float64: list_of_arrays_to_be_checked.append([the_occupations, 'occupations']) else: the_occupations = None # list_of_arrays_to_be_checked = [ [the_bands,'bands'] ] # check that there every element is a float if not the_bands.dtype.type == numpy.float64: list_of_arrays_to_be_checked.append([the_bands, 'bands']) for x, msg in list_of_arrays_to_be_checked: try: [float(_) for _ in x.flatten() if _ is not None] except (TypeError, ValueError): raise ValueError('The {} array can only contain float or None values'.format(msg)) # check the labels if labels is not None: if isinstance(labels, six.string_types): the_labels = [str(labels)] elif isinstance(labels, (tuple, list)) and all([isinstance(_, six.string_types) for _ in labels]): the_labels = [str(_) for _ in labels] else: raise ValidationError('Band labels have an unrecognized type ({})' 'but should be a string or a list of strings'.format(labels.__class__)) if len(the_bands.shape) == 2 and len(the_labels) != 1: raise ValidationError('More array labels than the number of arrays') elif len(the_bands.shape) == 3 and len(the_labels) != the_bands.shape: raise ValidationError('More array labels than the number of arrays') else: the_labels = None return the_bands, the_occupations, the_labels
[docs] def set_bands(self, bands, units=None, occupations=None, labels=None): """ Set an array of band energies of dimension (nkpoints x nbands). Kpoints must be set in advance. Can contain floats or None. :param bands: a list of nkpoints lists of nbands bands, or a 2D array of shape (nkpoints x nbands), with band energies for each kpoint :param units: optional, energy units :param occupations: optional, a 2D list or array of floats of same shape as bands, with the occupation associated to each band """ # checks bands and occupations the_bands, the_occupations, the_labels = self._validate_bands_occupations(bands, occupations, labels) # set bands and their units self.set_array('bands', the_bands) self.units = units if the_labels is not None: self.set_attribute('array_labels', the_labels) if the_occupations is not None: # set occupations self.set_array('occupations', the_occupations)
@property def array_labels(self): """ Get the labels associated with the band arrays """ return self.get_attribute('array_labels', None) @property def units(self): """ Units in which the data in bands were stored. A string """ # return copy.deepcopy(self._pbc) return self.get_attribute('units') @units.setter def units(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 """ the_str = str(value) self.set_attribute('units', the_str)
[docs] def _set_pbc(self, value): """ validate the pbc, then store them """ from aiida.common.exceptions import ModificationNotAllowed from aiida.orm.nodes.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_attribute('pbc1', the_pbc) self.set_attribute('pbc2', the_pbc) self.set_attribute('pbc3', the_pbc)
[docs] def get_bands(self, also_occupations=False, also_labels=False): """ Returns an array (nkpoints x num_bands or nspins x nkpoints x num_bands) of energies. :param also_occupations: if True, returns also the occupations array. Default = False """ try: bands = numpy.array(self.get_array('bands')) except KeyError: raise AttributeError('No stored bands has been found') to_return = [bands] if also_occupations: try: occupations = numpy.array(self.get_array('occupations')) except KeyError: raise AttributeError('No occupations were set') to_return.append(occupations) if also_labels: to_return.append(self.array_labels) if len(to_return) == 1: return bands else: return to_return
[docs] def _get_bandplot_data(self, cartesian, prettify_format=None, join_symbol=None, get_segments=False, y_origin=0.): """ Get data to plot a band structure :param cartesian: if True, distances (for the x-axis) are computed in cartesian coordinates, otherwise they are computed in reciprocal coordinates. cartesian=True will fail if no cell has been set. :param prettify_format: by default, strings are not prettified. If you want to prettify them, pass a valid prettify_format string (see valid options in the docstring of :py:func:prettify_labels). :param join_symbols: by default, strings are not joined. If you pass a string, this is used to join strings that are much closer than a given threshold. The most typical string is the pipe symbol: ``|``. :param get_segments: if True, also computes the band split into segments :param y_origin: if present, shift bands so to set the value specified at ``y=0`` :return: a plot_info dictiorary, whose keys are ``x`` (array of distances for the x axis of the plot); ``y`` (array of bands), ``labels`` (list of tuples in the format (float x value of the label, label string), ``band_type_idx`` (array containing an index for each band: if there is only one spin, then it's an array of zeros, of length equal to the number of bands at each point; if there are two spins, then it's an array of zeros or ones depending on the type of spin; the length is always equalt to the total number of bands per kpoint). """ # load the x and y's of the graph stored_bands = self.get_bands() if len(stored_bands.shape) == 2: bands = stored_bands band_type_idx = numpy.array( * stored_bands.shape) two_band_types = False elif len(stored_bands.shape) == 3: bands = numpy.concatenate([_ for _ in stored_bands], axis=1) band_type_idx = numpy.array( * stored_bands.shape +  * stored_bands.shape) two_band_types = True else: raise ValueError('Unexpected shape of bands') bands -= y_origin # here I build the x distances on the graph (in cartesian coordinates # if cartesian==True AND if the cell was set, otherwise in reciprocal # coordinates) try: kpoints = self.get_kpoints(cartesian=cartesian) except AttributeError: # this error is happening if cartesian==True and if no cell has been # set -> we switch to reciprocal coordinates to compute distances kpoints = self.get_kpoints() # I take advantage of the path to recognize discontinuities try: labels = self.labels labels_indices = [i for i in labels] except (AttributeError, TypeError): labels = [] labels_indices = [] # since I can have discontinuous paths, I set on those points the distance to zero # as a result, where there are discontinuities in the path, # I have two consecutive points with the same x coordinate distances = [ numpy.linalg.norm(kpoints[i] - kpoints[i - 1]) if not (i in labels_indices and i - 1 in labels_indices) else 0. for i in range(1, len(kpoints)) ] x = [float(sum(distances[:i])) for i in range(len(distances) + 1)] # transform the index of the labels in the coordinates of x raw_labels = [(x[i], i) for i in labels] the_labels = raw_labels if prettify_format: the_labels = prettify_labels(the_labels, format=prettify_format) if join_symbol: the_labels = join_labels(the_labels, join_symbol=join_symbol) plot_info = {} plot_info['x'] = x plot_info['y'] = bands plot_info['band_type_idx'] = band_type_idx plot_info['raw_labels'] = raw_labels plot_info['labels'] = the_labels if get_segments: plot_info['path'] = [] plot_info['paths'] = [] if len(labels) > 1: # I add an empty label that points to the first band if the first label does not do it if labels != 0: labels.insert(0, (0, '')) # I add an empty label that points to the last band if the last label does not do it if labels[-1] != len(bands)-1 : labels.append((len(bands)-1, '')) for (position_from, label_from), (position_to, label_to) in zip(labels[:-1], labels[1:]): if position_to - position_from > 1: # Create a new path line only if there are at least two points, # otherwise it is probably just a discontinuity point in the band # structure (e.g. Gamma-X|Y-Gamma), where X and Y would be two # consecutive points, but there is no path between them plot_info['path'].append([label_from, label_to]) path_dict = { 'length': position_to - position_from, 'from': label_from, 'to': label_to, 'values': bands[position_from:position_to + 1, :].transpose().tolist(), 'x': x[position_from:position_to + 1], 'two_band_types': two_band_types, } plot_info['paths'].append(path_dict) else: label_from = '0' label_to = '1' path_dict = { 'length': bands.shape - 1, 'from': label_from, 'to': label_to, 'values': bands.transpose().tolist(), 'x': x, 'two_band_types': two_band_types, } plot_info['paths'].append(path_dict) plot_info['path'].append([label_from, label_to]) return plot_info
{"ft_lang_label":"__label__en","ft_lang_prob":0.50869596,"math_prob":0.9338267,"size":56450,"snap":"2019-43-2019-47","text_gpt3_token_len":15632,"char_repetition_ratio":0.14488183,"word_repetition_ratio":0.20958406,"special_character_ratio":0.28864482,"punctuation_ratio":0.16974512,"nsfw_num_words":0,"has_unicode_error":false,"math_prob_llama3":0.9913074,"pos_list":[0],"im_url_duplicate_count":[null],"WARC_HEADER":"{\"WARC-Type\":\"response\",\"WARC-Date\":\"2019-10-15T02:05:48Z\",\"WARC-Record-ID\":\"<urn:uuid:44eebf71-b4aa-46d7-8a91-308b10753db1>\",\"Content-Length\":\"233752\",\"Content-Type\":\"application/http; msgtype=response\",\"WARC-Warcinfo-ID\":\"<urn:uuid:67b96543-b7f6-47c5-91a3-1258d83cf6f1>\",\"WARC-Concurrent-To\":\"<urn:uuid:5fcadee5-0ec7-4fef-a48b-348ca16319b7>\",\"WARC-IP-Address\":\"104.208.221.96\",\"WARC-Target-URI\":\"https://aiida-core.readthedocs.io/en/latest/_modules/aiida/orm/nodes/data/array/bands.html\",\"WARC-Payload-Digest\":\"sha1:6UD5X7REVKCHZNWKNDRTB2TQP2CXIHG6\",\"WARC-Block-Digest\":\"sha1:LSDVTVOEGN7EL6567HABQSTCVEJ7KVIT\",\"WARC-Identified-Payload-Type\":\"text/html\",\"warc_filename\":\"/cc_download/warc_2019/CC-MAIN-2019-43/CC-MAIN-2019-43_segments_1570986655735.13_warc_CC-MAIN-20191015005905-20191015033405-00110.warc.gz\"}"}