Source code for aiida_phonopy.data.phonopy

# -*- coding: utf-8 -*-
"""Module defining the class which wraps the :class:`phonopy.Phonopy` main class."""
from __future__ import annotations

from typing import Union

import numpy as np
from phonopy import Phonopy

from .preprocess import PreProcessData


[docs]class PhonopyData(PreProcessData): # pylint: disable=too-many-ancestors """Class wrapping the :class:`phonopy.Phonopy` class. It represents the final Data node status of a frozen phonon calculaiton. It stores information regarding the pre-processing, the displacements and forces dataset, and the (eventual) non-analytical constants. .. note: direct calculation of properties from this class is still not implemented. Use :class:`~aiida_phonopy.calculations.phonopy.PhonopyCalculation` for post-processing this data node, keeping the provenance and having the data produced in the correct data type. """ def __init__(self, preprocess_data: PreProcessData, **kwargs): """Instantiate the class. :param preprocess_data: a :class:`~aiida_phonopy.data.preprocess.PreProcessData` node :raises TypeError: if the input is not of the correct type, :raises ValueError: if the preprocess_data input does not contain displacement dataset """ if not isinstance(preprocess_data, PreProcessData): raise TypeError(f'incorrect type. Given type <{type(preprocess_data)}>, expected `PreProcessData') kwargs['structure'] = preprocess_data.get_unitcell() kwargs['supercell_matrix'] = preprocess_data.supercell_matrix kwargs['primitive_matrix'] = preprocess_data.primitive_matrix kwargs['symprec'] = preprocess_data.symprec kwargs['is_symmetry'] = preprocess_data.is_symmetry kwargs['distinguish_kinds'] = preprocess_data.distinguish_kinds super().__init__(**kwargs) dataset = preprocess_data.displacement_dataset if dataset is not None: super().set_displacements_from_dataset(dataset) else: raise ValueError('cannot instantiate object without having displacement dataset set')
[docs] def set_displacements(self): """Set displacements cannot be accessed from PhonopyData.""" raise RuntimeError('`displacements` cannot be changed for this Data')
[docs] def set_displacements_from_dataset(self): """Set displacements cannot be accessed from PhonopyData.""" raise RuntimeError('`displacements` cannot be changed for this Data')
[docs] def get_phonopy_instance(self, subtract_residual_forces: bool | None = None, **kwargs) -> Phonopy: """Return a :class:`~phonopy.Phonopy` object with forces and nac parameters (if set). :param subtract_residual_forces: whether or not subract residual forces (if set) :type subtract_residual_forces: bool :param kwargs: see :func:`aiida_phonopy.data.preprocess.PreProcessData.get_phonopy_instance` * symmetrize_nac: whether or not to symmetrize the nac parameters using point group symmetry; bool, defaults to self.is_symmetry * factor_nac: factor for non-analytical corrections; float, defaults to Hartree*Bohr """ if not isinstance(subtract_residual_forces, bool) and subtract_residual_forces is not None: raise TypeError('`subtract_residual_forces` not of the right type') if subtract_residual_forces and self.residual_forces is None: raise ValueError('`subtract_residual_forces` is set to True, but `residual_forces` are None') ph_instance = super().get_phonopy_instance(**kwargs) if self.displacement_dataset is not None: ph_instance.dataset = self.displacement_dataset try: if self.forces is not None: the_forces = self.forces if subtract_residual_forces: the_forces = the_forces - self.residual_forces ph_instance.forces = the_forces except AttributeError: pass return ph_instance
@property
[docs] def residual_forces(self) -> np.ndarray: """Get the residual forces calculated on the pristine (i.e. no displaced) supercell structure (if set). ..note: if you have specified the `forces_index` this will be used as well here. """ try: if self.forces_index is not None: the_forces = self.get_array('residual_forces')[self.forces_index] else: the_forces = self.get_array('residual_forces') except KeyError: the_forces = None return the_forces
[docs] def set_residual_forces(self, forces: Union[list, np.ndarray]): """Set the residual forces of the pristine supercell. :param forces: (atoms in supercell, 3) array shape :raises: * TypeError: if the format is not of the correct type * ValueError: if the format is not compatible """ self._if_can_modify() if not isinstance(forces, (list, np.ndarray)): raise TypeError('the input is not of the correct type') natoms = len(self.get_supercell().sites) the_forces = np.array(forces) if self.forces_index is not None: if the_forces[self.forces_index].shape == (natoms, 3): self.set_array('residual_forces', the_forces) else: raise ValueError('the array is not of the correct shape') else: if the_forces.shape == (natoms, 3): self.set_array('residual_forces', the_forces) else: raise ValueError('the array is not of the correct shape. Check also `forces_index`')
@property
[docs] def forces(self) -> np.ndarray: """Get forces for each supercell with displacements in the dataset as a unique array.""" try: the_forces = self.get_array('forces') return the_forces except (KeyError, AttributeError): pass if not 'forces_1' in self.get_arraynames(): return try: nsupercells = len(self.displacements) the_forces = np.zeros((nsupercells)).tolist() for i in range(nsupercells): if self.forces_index is not None: the_forces[i] = self.get_array(f'forces_{i+1}')[self.forces_index] else: the_forces[i] = self.get_array(f'forces_{i+1}') the_forces = np.array(the_forces) except (KeyError, AttributeError): return return the_forces
[docs] def set_forces( self, sets_of_forces: Union[list, np.ndarray, None] = None, dict_of_forces: dict | None = None, forces_index: int | None = None ): """Set forces per each supercell with displacement in the dataset. :param sets_of_forces: a set of atomic forces in displaced supercells. The order of displaced supercells has to match with that in displacement dataset. :param type: (supercells with displacements, atoms in supercell, 3) array shape :param dict_of_forces: dictionary of forces, in numpy.ndarray to store for each displacement. They keys for the dictionary must be passed as `forces_{num}`, where `num` corresponds to the associated supercell in the dataset. `num` starts from 1. :param forces_index: an integer storing in the database the index for forces. The `dict_of_forces` may be specified from `TrajectoryData` to reduce the amount of data saved in the repository. For example: forces_1 = [[actual array]] ==> forces_index = 0 :raises: * TypeError: if the format is not of the correct type * ValueError: if the format is not compatible * RuntimeError: if the displacement dataset was not initialize in input """ self._if_can_modify() if self.displacement_dataset is None: raise RuntimeError('the displacement dataset has not been set yet') nsupercells = len(self.displacements) natoms = len(self.get_supercell().sites) if sets_of_forces is not None: the_forces = np.array(sets_of_forces) if the_forces.shape == (nsupercells, natoms, 3): self.set_array('forces', the_forces) else: raise ValueError('the array is not of the correct shape') if dict_of_forces is not None: # First, verify the number of keys is correct if len(list(dict_of_forces.keys())) != nsupercells: raise ValueError('the dictionary does not have the correct number of forces') # Second, verify the keys for key in dict_of_forces.keys(): if key.split('_')[0] != 'forces': raise ValueError(f'{key} is not correct. Expected `forces_num` as key') # Third, store for key, value in dict_of_forces.items(): new_key = f"forces_{int(key.split('_')[-1])}" self.set_array(new_key, np.array(value)) if forces_index is not None: if isinstance(forces_index, int): self.base.attributes.set('forces_index', forces_index) else: raise ValueError('index for forces must be an integer')
@property
[docs] def forces_index(self) -> int: """Return the index of the forces to use.""" try: index = self.base.attributes.get('forces_index') except (KeyError, AttributeError): index = None return index
[docs] def set_forces_index(self, value: int): """Set the `forces_index` attribute. This is used for tacking a particular array index of each single forces set. This is useful to not duplicate data of e.g. trajectories. """ if isinstance(value, int): self.base.attributes.set('forces_index', value) else: raise ValueError('index for forces must be an integer')