1# coding: utf-8 2"""Objects to run and analyze the frozen phonons generated from displacements of atoms""" 3 4import numpy as np 5import scipy.optimize as optimize 6 7from monty.functools import lazy_property 8from monty.collections import dict2namedtuple 9from abipy.core.abinit_units import phfactor_ev2units, amu_emass, Bohr_Ang, eV_Ha 10from abipy.tools.plotting import add_fig_kwargs, get_ax_fig_plt 11 12 13def quadratic_fit_function(xx, aa, bb): 14 """ 15 Simple quadratic function used as a default for the fitting. 16 17 Args: 18 xx: the variable 19 aa: the coefficient of the quadratic term 20 bb: the constant term 21 """ 22 return aa * xx ** 2 + bb 23 24 25class FrozenPhonon(object): 26 """ 27 Class defining a set of structures with displaced atoms. 28 Provides methods to generate, interpolate and plot the data. 29 """ 30 31 def __init__(self, original_structure, original_displ_cart, structures, normalized_displ_cart, etas, 32 qpt_frac_coords, scale_matrix, energies=None): 33 """ 34 35 Args: 36 original_structure: the original |Structure| object without any displacement. 37 original_displ_cart: the original displacement used for generating the structures in cartesian 38 coordinates. Should be linked to original_structure and should contain the phdispl_cart obtained 39 from the phonon calculation. 40 structures: list of structures with the displaced atoms 41 normalized_displ_cart: displacement applied to the supercell to generate the list of structures. 42 The normalization defines the largest displacement of one atom at 1 Angstrom. 43 etas: list of (signed) factors that will multiply the normalized displacement to obtain the list of 44 structure. 45 qpt_frac_coords: fractional coordinates of the qpoint corresponding to the displacement. 46 scale_matrix: the scaling matrix between the original cell and the supercell. 47 energies: energies in eV corresponding to the structures. 48 """ 49 50 self.original_structure = original_structure 51 self.original_displ_cart = original_displ_cart 52 self.structures = structures 53 self.normalized_displ_cart = normalized_displ_cart 54 self.etas = np.array(etas) 55 self.qpt_frac_coords = qpt_frac_coords 56 self.scale_matrix = scale_matrix 57 self._energies = energies 58 59 @property 60 def energies(self): 61 """ 62 Energies in eV corresponding to the the structures. 63 """ 64 return self._energies 65 66 @energies.setter 67 def energies(self, energies): 68 """ 69 Set the number of energies corresponding to the structures, should have the same length as structures 70 """ 71 if len(energies) != self.n_displ: 72 raise ValueError("The size of the enegies list does not match the number of internal structures.") 73 self._energies = energies 74 75 @property 76 def qpt_cart_coords(self): 77 """ 78 The cartesian coordinates of the qpoint. 79 """ 80 return self.original_structure.lattice.reciprocal_lattice.get_cartesian_coords(self.qpt_frac_coords) 81 82 @property 83 def n_displ(self): 84 """ 85 Number of displacements. 86 """ 87 return len(self.structures) 88 89 @lazy_property 90 def ieta0(self): 91 """ 92 The index corresponding to the structure with no displacements. 93 """ 94 try: 95 return self.etas.tolist().index(0) 96 except ValueError: 97 raise ValueError("The structure with no displacement is not present in the list.") 98 99 @classmethod 100 def from_phbands(cls, phbands, qpt_frac_coords, imode, etas, scale_matrix=None, max_supercell=None): 101 """ 102 Create an instace of FrozenPhonon using the eigendisplacements from a |PhononBands| 103 104 Args: 105 phbands: a |PhononBands| instance. 106 qpt_frac_coords: q vector in reduced coordinate in reciprocal space or index of the qpoint. 107 imode: index of the mode. 108 etas: list of amplitudes of the displacement to be applied to the system. Will correspond to the 109 largest displacement of one atom in Angstrom. 110 scale_matrix: the scaling matrix of the supercell. If None a scaling matrix suitable for 111 the qpoint will be determined. 112 max_supercell: mandatory if scale_matrix is None, ignored otherwise. Defines the largest 113 supercell in the search for a scaling matrix suitable for the q point. 114 115 Returns: 116 A FrozenPhonon. 117 """ 118 119 qind = phbands.qindex(qpt_frac_coords) 120 original_displ_cart = phbands.phdispl_cart[qind, imode].reshape((-1, 3)) 121 122 # first extract data with the normalized displacement (eta=1) 123 normalized_fp = phbands.get_frozen_phonons(qind, imode, 1, scale_matrix, max_supercell) 124 125 structures = [] 126 for n in etas: 127 structures.append(phbands.get_frozen_phonons(qind, imode, n, normalized_fp.scale_matrix).structure) 128 129 return cls(phbands.structure, original_displ_cart, structures, normalized_fp.displ, etas, 130 phbands.qpoints[qind].frac_coords, normalized_fp.scale_matrix) 131 132 @lazy_property 133 def mass_factor(self): 134 """ 135 The factor accounting for the different masses and displacement of each atom 136 """ 137 # the norms of each atomic displacement of the normalized displacement 138 displ_norms = np.linalg.norm(self.normalized_displ_cart, axis=1) 139 140 mass_factor = sum(site.specie.atomic_mass * (df**2) 141 for site, df in zip(self.structures[0], displ_norms / displ_norms.max())) / 2. 142 mass_factor *= amu_emass 143 return mass_factor 144 145 def _freq_to_quad_coeff(self, freq): 146 """ 147 Helper function to convert from a frequency in eV to the quadratic coefficient corresponding to the 148 energies in eV and the etas in Angstrom. Uses the conversions to a.u. to get the correct value. 149 """ 150 151 return freq ** 2 * self.mass_factor * eV_Ha / Bohr_Ang ** 2 152 153 def _quad_coeff_to_freq(self, coeff): 154 """ 155 Helper function to convert from the quadratic coefficient corresponding to the fit of energies in eV and 156 the etas in Angstrom to a frequency in eV. Uses the conversions to a.u. to get the correct value. 157 """ 158 159 return np.sqrt(coeff / self.mass_factor / eV_Ha) * Bohr_Ang 160 161 def fit_to_frequency(self, fit_function=None, units="eV", min_fit_eta=None, max_fit_eta=None): 162 """ 163 Uses the energies and the displacements to calculate the phonon frequency corresponding to the quadratic 164 term of the fit. 165 The fit is performed with scipy.optimize.curve_fit based on the function given in input and can also be 166 limited number to a subset of the values of the displacements. 167 168 Args: 169 fit_function: a function that will be used to fit the data. The first parameter should be the coefficient 170 of the quadratic term. If None a simple quadratic fit will be used. 171 units: units of the output frequency. Possible values in ("eV", "meV", "Ha", "cm-1", "Thz"). 172 Case-insensitive. 173 min_fit_eta: if not None represents minimum value allowed for the (signed) eta to be used in the fit. 174 max_fit_eta: if not None represents maximum value allowed for the (signed) eta to be used in the fit. 175 176 Returns: 177 A namedtuple with 'freq': the values of the frequency extracted from the fit, 178 'fit_params': the parameters obtained from the fit, 'cov': the estimated covariance of fit_params 179 (see scipy.optimize.curve_fit documentation for more details). 180 """ 181 182 if self.energies is None: 183 raise ValueError("The energies are required to calculate the fit") 184 185 if min_fit_eta is None: 186 min_fit_eta = self.etas.min() 187 if max_fit_eta is None: 188 max_fit_eta = self.etas.max() 189 190 indices = np.where((min_fit_eta <= self.etas) & (self.etas <= max_fit_eta)) 191 192 if fit_function is None: 193 fit_function = quadratic_fit_function 194 195 etas = self.etas[indices] 196 energies = np.array(self.energies)[indices] 197 198 params, cov = optimize.curve_fit(fit_function, etas, energies) 199 200 # frequency in eV. 201 freq = self._quad_coeff_to_freq(params[0]) 202 203 return dict2namedtuple(freq=freq*phfactor_ev2units(units), fit_params=params, cov=cov) 204 205 @add_fig_kwargs 206 def plot_fit_energies(self, fit_function=None, min_fit_eta=None, max_fit_eta=None, freq=None, 207 ax=None, **kwargs): 208 """ 209 Fits the displacements etas to the energies. See fit_to_frequency() for more details. 210 211 Args: 212 fit_function: a function that will be used to fit the data. The first parameter should be the coefficient 213 of the quadratic term. If None a simple quadratic fit will be used. 214 min_fit_eta: if not None represents minimum value allowed for the (signed) eta to be used in the fit. 215 max_fit_eta: if not None represents maximum value allowed for the (signed) eta to be used in the fit. 216 freq: if not None the quadratic function with this frequency as a coefficient will be added to the plot. 217 freq in eV. Requires the 0 displacement to be present in the list of etas. 218 ax: |matplotlib-Axes| or None if a new figure should be created. 219 220 Returns: 221 |matplotlib-Figure| 222 """ 223 224 if fit_function is None: 225 fit_function = quadratic_fit_function 226 227 fit_data = self.fit_to_frequency(fit_function, min_fit_eta=min_fit_eta, max_fit_eta=max_fit_eta) 228 229 ax, fig, plt = get_ax_fig_plt(ax=ax) 230 231 if "color" not in kwargs and "c" not in kwargs: 232 kwargs["color"] = "blue" 233 234 kwargs_points = dict(kwargs) 235 kwargs_points["linestyle"] = "None" 236 if "markersize" not in kwargs_points and "ms" not in kwargs_points: 237 kwargs_points["markersize"] = 10 238 if "marker" not in kwargs_points and "m" not in kwargs_points: 239 kwargs_points["marker"] = "o" 240 241 ax.plot(self.etas, self.energies, **kwargs_points) 242 243 if "linewidth" not in kwargs and "lw" not in kwargs: 244 kwargs["linewidth"] = 2 245 kwargs["linestyle"] = "-" 246 247 etas_plot = np.linspace(self.etas.min(), self.etas.max(), 100) 248 ax.plot(etas_plot, fit_function(etas_plot, *fit_data.fit_params), **kwargs) 249 250 if freq is not None: 251 kwargs["linestyle"] = "--" 252 e0 = self.energies[self.ieta0] 253 en_freq = quadratic_fit_function(etas_plot, self._freq_to_quad_coeff(freq), e0) 254 ax.plot(etas_plot, en_freq, **kwargs) 255 256 ax.set_xlabel("Displacements (Ang)") 257 ax.set_ylabel("Energy (eV)") 258 259 return fig 260 261 @add_fig_kwargs 262 def plot_anharmonic_contribution(self, freq, relative=False, ax=None, **kwargs): 263 """ 264 Plots the the absolute relative difference between the energies extracted from the frequency as 265 quadratic coefficient and the calculated energies, giving an estimate of the anharmonic contribution 266 Requires the 0 displacement to be present in the list of etas. 267 268 Args: 269 freq: phonon frequncy in eV 270 relative: if True the plot will represent the relative difference with respect to the expected value 271 obtained from the frequency, rather than the absolute difference. 272 ax: |matplotlib-Axes| or None if a new figure should be created. 273 274 Returns: 275 |matplotlib-Figure| 276 """ 277 278 if self.energies is None: 279 raise ValueError("The energies are required to calculate the fit") 280 281 self._freq_to_quad_coeff(freq) 282 283 if "color" not in kwargs and "c" not in kwargs: 284 kwargs["color"] = "blue" 285 if "linewidth" not in kwargs and "lw" not in kwargs: 286 kwargs["linewidth"] = 2 287 288 e0 = self.energies[self.ieta0] 289 en_freq = quadratic_fit_function(self.etas,self._freq_to_quad_coeff(freq), e0) 290 291 diff = np.abs(en_freq - np.array(self.energies)) 292 293 if relative: 294 diff = [d/(e-e0) * 100 if e != 0 else 0 for d, e in zip(diff, en_freq)] 295 296 ax, fig, plt = get_ax_fig_plt(ax=ax) 297 298 ax.plot(self.etas, diff, **kwargs) 299 300 ax.set_xlabel("Displacements (Ang)") 301 if relative: 302 ax.set_ylabel("Relative energy difference (%)") 303 else: 304 ax.set_ylabel("Energy difference (eV)") 305 306 return fig 307