Source code for aiida_quantumespresso.data.force_constants

# -*- coding: utf-8 -*-
"""Sub class of `Data` to handle interatomic force constants produced by the Quantum ESPRESSO q2r.x code."""
from aiida.orm import SinglefileData
import numpy
from qe_tools import CONSTANTS


[docs]class ForceConstantsData(SinglefileData): """Class to handle interatomic force constants from the Quantum ESPRESSO q2r.x code."""
[docs] def set_file(self, file, filename=None, **kwargs): """Add a file to the node, parse it and set the attributes found. :param file: absolute path to the file or a filelike object :param filename: specify filename to use (defaults to name of provided file). """ # pylint: disable=redefined-builtin super().set_file(file, filename, **kwargs) # Parse the force constants file dictionary, _, _ = parse_q2r_force_constants_file(self.get_content().splitlines(), also_force_constants=False) # Add all other attributes found in the parsed dictionary for key, value in dictionary.items(): self.base.attributes.set(key, value)
@property
[docs] def number_of_species(self): """Return the number of atom species. :return: a scalar """ return self.base.attributes.get('number_of_species')
@property
[docs] def number_of_atoms(self): """Return the number of atoms. :return: a scalar """ return self.base.attributes.get('number_of_atoms')
@property
[docs] def cell(self): """Return the crystal unit cell where rows are the crystal vectors. :return: a 3x3 numpy.array """ return numpy.array(self.base.attributes.get('cell'))
@property
[docs] def atom_list(self): """Return the list of atoms. :return: a list of length-5 tuple (element name, element mass amu_ry, 3 coordinates in cartesian Angstrom) """ return self.base.attributes.get('atom_list')
@property
[docs] def has_done_electric_field(self): """Return flag to indicate if dielectric tensor and effective charges were computed. :return: a boolean """ return self.base.attributes.get('has_done_electric_field')
@property
[docs] def dielectric_tensor(self): """Return the dielectric tensor matrix. :return: a 3x3 tuple """ return self.base.attributes.get('dielectric_tensor')
@property
[docs] def effective_charges_eu(self): """Return the effective charges for each atom. :return: a list of number_of_atoms elements, each being a 3x3 tuple """ return self.base.attributes.get('effective_charges_eu')
@property
[docs] def qpoints_mesh(self): """Return the number of q-points in each direction. :return: a length-3 tuple """ return tuple(self.base.attributes.get('qpoints_mesh'))
[docs]def parse_q2r_force_constants_file(lines, also_force_constants=False): """Parse the real-space interatomic force constants file from QE-Q2R. :param also_force_constants: True to parse the force constants as well :return parsed_data: dictionary with the following keywords: - number_of_species: number of atom species ('ntyp' in QE) - number_of_atoms: number of atoms ('nat' in QE) - cell: unit cell - atom_list: list with, for each atom in the cell, a length-5 tuple of the form (element_name, mass_in_amu_ry, then 3 coordinates in cartesian & Angstroms) - has_done_electric_field: True if dielectric constants & effective charges were computed - dielectric_tensor: dielectric constant (3x3 matrix) - effective_charges_eu: effective charges (ntyp x 3 x 3 matrix) - qpoints_mesh: length-3 tuple with number of qpoints in each dimension of the reciprocal lattice - force_constants: the real-space force constants: array with 7 indices, of the kind C(mi1, mi2, mi3, ji1, ji2, na1, na2) with * (mi1, mi2, mi3): the supercell dimensions * (ji1, ji2): axis of the displacement of the two atoms (from 1 to 3) * (na1, na2): atom numbers in the cell. - warnings: a list of warnings :return force_constants: the real-space force constants: array with 7 indices, of the kind C(mi1, mi2, mi3, ji1, ji2, na1, na2) where: * (mi1, mi2, mi3): the supercell dimensions * (ji1, ji2): axis of the displacement of the two atoms (from 1 to 3) * (na1, na2): atom numbers in the cell. """ # pylint: disable=too-many-statements,too-many-branches,too-many-nested-blocks parsed_data = {} warnings = [] try: # read first line current_line = 0 first_line = lines[current_line].split() ntyp = int(first_line[0]) nat = int(first_line[1]) ibrav = int(first_line[2]) celldm = [float(c) for c in first_line[3:]] if len(celldm) != 6: warnings.append('Wrong length for celldm') if ibrav != 0: warnings.append(f'ibrav ({ibrav}) is not 0; q-points path for phonon dispersion might be wrong') if any(item != 0 for item in celldm[1:]): warnings.append('celldm[1:] are not all zero; only celldm[0] will be used') parsed_data['number_of_species'] = ntyp parsed_data['number_of_atoms'] = nat current_line += 1 # read cell data cell = tuple( tuple(float(c) * celldm[0] * CONSTANTS.bohr_to_ang for c in l.split()) for l in lines[current_line:current_line + 3] ) parsed_data['cell'] = cell current_line += 3 # read atom types and masses atom_type_list = [] for ityp in range(ntyp): line = lines[current_line].split("'") if int(line[0]) == ityp + 1: atom_type_list.append(tuple((line[1].strip(), float(line[2])))) current_line += 1 # read each atom coordinates atom_list = [] for _ in range(nat): line = [float(c) for c in lines[current_line].split()] ityp = int(line[1]) if 0 < ityp < ntyp + 1: line[0] = atom_type_list[ityp - 1][0] # string with element name line[1] = atom_type_list[ityp - 1][1] # element mass in amu_ry # Convert atomic positions (in cartesian) from alat to Angstrom: line[2:] = [pos * celldm[0] * CONSTANTS.bohr_to_ang for pos in line[2:]] atom_list.append(tuple(line)) current_line += 1 parsed_data['atom_list'] = atom_list # read lrigid (flag for dielectric constant and effective charges has_done_electric_field = lines[current_line].split()[0] == 'T' parsed_data['has_done_electric_field'] = has_done_electric_field current_line += 1 if has_done_electric_field: # read dielectric tensor dielectric_tensor = tuple(tuple(float(c) for c in l.split()) for l in lines[current_line:current_line + 3]) current_line += 3 effective_charges_eu = [] for _ in range(nat): current_line += 1 effective_charges_eu.append( tuple(tuple(float(c) for c in l.split()) for l in lines[current_line:current_line + 3]) ) current_line += 3 parsed_data['dielectric_tensor'] = dielectric_tensor parsed_data['effective_charges_eu'] = effective_charges_eu # read q-points mesh qpoints_mesh = tuple(int(c) for c in lines[current_line].split()) current_line += 1 parsed_data['qpoints_mesh'] = qpoints_mesh force_constants = () if also_force_constants: # read force_constants force_constants = numpy.zeros(qpoints_mesh + (3, 3, nat, nat), dtype=float) for ji1 in range(3): for ji2 in range(3): for na1 in range(nat): for na2 in range(nat): indices = tuple(int(c) for c in lines[current_line].split()) current_line += 1 if (ji1 + 1, ji2 + 1, na1 + 1, na2 + 1) != indices: raise ValueError('Wrong indices in force constants') for mi3 in range(qpoints_mesh[2]): for mi2 in range(qpoints_mesh[1]): for mi1 in range(qpoints_mesh[0]): line = lines[current_line].split() indices = tuple(int(c) for c in line[:3]) if (mi1 + 1, mi2 + 1, mi3 + 1) != indices: raise ValueError('Wrong supercell indices in force constants') force_constants[mi1, mi2, mi3, ji1, ji2, na1, na2] = float(line[3]) current_line += 1 except (IndexError, ValueError) as exc: raise ValueError(str(exc) + '\nForce constants file could not be parsed (incorrect file format)') from exc return parsed_data, force_constants, warnings