1# coding: utf-8 2""" 3Python API for the DDB file containing the derivatives of the total Energy wrt different perturbations. 4""" 5import sys 6import os 7import tempfile 8import itertools 9import numpy as np 10import pandas as pd 11import abipy.core.abinit_units as abu 12 13from collections import OrderedDict 14from functools import lru_cache 15from monty.string import marquee, list_strings 16from monty.json import MSONable 17from monty.collections import AttrDict, dict2namedtuple 18from monty.functools import lazy_property 19from monty.termcolor import cprint 20from monty.dev import deprecated 21from pymatgen.core.units import eV_to_Ha, bohr_to_angstrom, Energy 22from pymatgen.util.serialization import pmg_serialize 23from abipy.flowtk import AnaddbTask 24from abipy.core.mixins import TextFile, Has_Structure, NotebookWriter 25from abipy.core.symmetries import AbinitSpaceGroup 26from abipy.core.structure import Structure 27from abipy.core.kpoints import KpointList, Kpoint 28from abipy.iotools import ETSF_Reader 29from abipy.tools.numtools import data_from_cplx_mode 30from abipy.abio.inputs import AnaddbInput 31from abipy.dfpt.phonons import PhononDosPlotter, PhononBandsPlotter 32from abipy.dfpt.ifc import InteratomicForceConstants 33from abipy.dfpt.elastic import ElasticData 34from abipy.dfpt.raman import Raman 35from abipy.core.abinit_units import phfactor_ev2units, phunit_tag 36from abipy.tools.plotting import add_fig_kwargs, get_ax_fig_plt, get_axarray_fig_plt 37from abipy.tools import duck 38from abipy.tools.iotools import ExitStackWithFiles 39from abipy.tools.tensors import DielectricTensor, ZstarTensor, Stress 40from abipy.abio.robots import Robot 41 42 43class DdbError(Exception): 44 """Error class raised by DDB.""" 45 46 47class AnaddbError(DdbError): 48 """ 49 Exceptions raised when we try to execute |AnaddbTask| in the |DdbFile| methods 50 51 An `AnaddbError` has a reference to the task and to the :class:`EventsReport` that contains 52 the error messages of the run. 53 """ 54 def __init__(self, *args, **kwargs): 55 self.task, self.report = kwargs.pop("task"), kwargs.pop("report") 56 super().__init__(*args, **kwargs) 57 58 def __str__(self): 59 lines = ["""\n 60 An exception has been raised while executing anaddb in workdir: %s 61 Please check the run.err, the run.abo and the job.sh files in the workdir 62 and make sure that manager.yml is properly configured. 63""" 64 % self.task.workdir] 65 app = lines.append 66 67 if self.report.errors: 68 app("Found %d errors" % len(self.report.errors)) 69 lines += [str(err) for err in self.report.errors] 70 71 return "\n".join(lines) 72 73 74class DdbFile(TextFile, Has_Structure, NotebookWriter): 75 """ 76 This object provides an interface to the DDB_ file produced by ABINIT 77 as well as methods to compute phonon band structures, phonon DOS, thermodynamic properties etc. 78 79 About the indices (idir, ipert) used by Abinit (Fortran notation): 80 81 * idir in [1, 2, 3] gives the direction (usually reduced direction, cart for strain) 82 * ipert in [1, 2, ..., mpert] where mpert = natom + 6 83 84 * ipert in [1, ..., natom] corresponds to atomic perturbations (reduced directions) 85 * ipert = natom + 1 corresponds d/dk (reduced directions) 86 * ipert = natom + 2 corresponds the electric field 87 * ipert = natom + 3 corresponds the uniaxial stress (Cartesian directions) 88 * ipert = natom + 4 corresponds the shear stress. (Cartesian directions) 89 90 .. rubric:: Inheritance 91 .. inheritance-diagram:: DdbFile 92 """ 93 Error = DdbError 94 AnaddbError = AnaddbError 95 96 @classmethod 97 def from_file(cls, filepath): 98 """Needed for the :class:`TextFile` abstract interface.""" 99 return cls(filepath) 100 101 @classmethod 102 def from_string(cls, string): 103 """Build object from string using temporary file.""" 104 fd, tmp_filepath = tempfile.mkstemp(text=True, prefix="_DDB") 105 with open(tmp_filepath, "wt") as fh: 106 fh.write(string) 107 108 return cls(tmp_filepath) 109 110 @classmethod 111 def from_mpid(cls, material_id, api_key=None, endpoint=None): 112 """ 113 Fetch DDB file corresponding to a materials project ``material_id``, 114 save it to temporary file and return new DdbFile object. 115 116 Raises: MPRestError if DDB file is not available 117 118 Args: 119 material_id (str): Materials Project material_id (e.g., mp-1234). 120 api_key (str): A String API key for accessing the MaterialsProject REST interface. 121 If None, the code will check if there is a `PMG_MAPI_KEY` in your .pmgrc.yaml. 122 endpoint (str): Url of endpoint to access the MaterialsProject REST interface. 123 Defaults to the standard Materials Project REST address 124 """ 125 from abipy.core import restapi 126 with restapi.get_mprester(api_key=api_key, endpoint=endpoint) as rest: 127 ddb_string = rest._make_request("/materials/%s/abinit_ddb" % material_id) 128 129 _, tmpfile = tempfile.mkstemp(prefix=material_id, suffix='_DDB') 130 with open(tmpfile, "wt") as fh: 131 fh.write(ddb_string) 132 133 return cls(tmpfile) 134 135 @classmethod 136 def as_ddb(cls, obj): 137 """ 138 Return an instance of |DdbFile| from a generic object `obj`. 139 Accepts: DdbFile or filepath 140 """ 141 return obj if isinstance(obj, cls) else cls.from_file(obj) 142 143 def __init__(self, filepath): 144 super().__init__(filepath) 145 146 self._header = self._parse_header() 147 148 self._structure = Structure.from_abivars(**self.header) 149 # Add AbinitSpacegroup (needed in guessed_ngkpt) 150 # FIXME: kptopt is not reported in the header --> has_timerev is always set to True 151 spgid, has_timerev, h = 0, True, self.header 152 self._structure.set_abi_spacegroup(AbinitSpaceGroup(spgid, h.symrel, h.tnons, h.symafm, has_timerev)) 153 154 # Add forces to structure. 155 if self.cart_forces is not None: 156 self._structure.add_site_property("cartesian_forces", self.cart_forces) 157 158 frac_coords = self._read_qpoints() 159 self._qpoints = KpointList(self.structure.lattice.reciprocal_lattice, frac_coords, weights=None, names=None) 160 161 def __str__(self): 162 """String representation.""" 163 return self.to_string() 164 165 def to_string(self, verbose=0): 166 """String representation.""" 167 lines = [] 168 app, extend = lines.append, lines.extend 169 170 app(marquee("File Info", mark="=")) 171 app(self.filestat(as_string=True)) 172 app("") 173 app(self.structure.to_string(verbose=verbose, title="Structure")) 174 app("") 175 app(marquee("DDB Info", mark="=")) 176 app("") 177 app("Number of q-points in DDB: %d" % len(self.qpoints)) 178 app("guessed_ngqpt: %s (guess for the q-mesh divisions made by AbiPy)" % self.guessed_ngqpt) 179 #if verbose: 180 h = self.header 181 #app("Important parameters extracted from the header:") 182 app("ecut = %f, ecutsm = %f, nkpt = %d, nsym = %d, usepaw = %d" % (h.ecut, h.ecutsm, h.nkpt, h.nsym, h.usepaw)) 183 app("nsppol %d, nspinor %d, nspden %d, ixc = %d, occopt = %d, tsmear = %f" % ( 184 h.nsppol, h.nspinor, h.nspden, h.ixc, h.occopt, h.tsmear)) 185 app("") 186 187 app("Has total energy: %s, Has forces: %s" % ( 188 self.total_energy is not None, self.cart_forces is not None)) 189 if self.total_energy is not None: 190 app("Total energy: %s [eV]" % self.total_energy) 191 #app("Has forces: %s" % ( 192 #if self.cart_forces is not None: 193 # app("Cartesian forces (eV/Ang):\n%s" % (self.cart_forces)) 194 # app("") 195 if self.cart_stress_tensor is not None: 196 app("") 197 app("Cartesian stress tensor in GPa with pressure %.3e (GPa):\n%s" % ( 198 - self.cart_stress_tensor.trace() / 3, self.cart_stress_tensor)) 199 else: 200 app("Has stress tensor: %s" % (self.cart_stress_tensor is not None)) 201 app("") 202 app("Has (at least one) atomic pertubation: %s" % self.has_at_least_one_atomic_perturbation()) 203 #app("Has (at least one) electric-field perturbation: %s" % self.has_epsinf_terms(select="at_least_one")) 204 #app("Has (all) electric-field perturbation: %s" % self.has_epsinf_terms(select="all")) 205 app("Has (at least one diagonal) electric-field perturbation: %s" % 206 self.has_epsinf_terms(select="at_least_one_diagoterm")) 207 app("Has (at least one) Born effective charge: %s" % self.has_bec_terms(select="at_least_one")) 208 app("Has (all) strain terms: %s" % self.has_strain_terms(select="all")) 209 app("Has (all) internal strain terms: %s" % self.has_internalstrain_terms(select="all")) 210 app("Has (all) piezoelectric terms: %s" % self.has_piezoelectric_terms(select="all")) 211 212 if verbose: 213 # Print q-points 214 app(self.qpoints.to_string(verbose=verbose, title="Q-points in DDB")) 215 216 if verbose > 1: 217 # Print full header. 218 from pprint import pformat 219 app(marquee("DDB header", mark="=")) 220 app(pformat(self.header)) 221 222 return "\n".join(lines) 223 224 def get_string(self): 225 """Return string with DDB content.""" 226 with open(self.filepath, "rt") as fh: 227 return fh.read() 228 229 @property 230 def structure(self): 231 """|Structure| object.""" 232 return self._structure 233 234 @property 235 def natom(self): 236 """Number of atoms in structure.""" 237 return len(self.structure) 238 239 @property 240 def version(self): 241 """DDB Version number (integer).""" 242 return self.header["version"] 243 244 @property 245 def header(self): 246 """ 247 Dictionary with the values reported in the header section. 248 Use e.g. ``ddb.header.ecut`` to access its values 249 """ 250 return self._header 251 252 def _parse_header(self): 253 """Parse the header sections. Returns |AttrDict| dictionary.""" 254 #ixc 7 255 #kpt 0.00000000000000D+00 0.00000000000000D+00 0.00000000000000D+00 256 # 0.25000000000000D+00 0.00000000000000D+00 0.00000000000000D+00 257 self.seek(0) 258 keyvals = [] 259 header_lines = [] 260 for i, line in enumerate(self): 261 header_lines.append(line.rstrip()) 262 line = line.strip() 263 if not line: continue 264 if "Version" in line: 265 # +DDB, Version number 100401 266 version = int(line.split()[-1]) 267 268 if line in ("Description of the potentials (KB energies)", 269 "No information on the potentials yet", 270 "Description of the PAW dataset(s)"): 271 # Skip section with psps info. 272 break 273 274 # header starts here 275 if i >= 6: 276 # Python does not support exp format with D 277 line = line.replace("D+", "E+").replace("D-", "E-") 278 tokens = line.split() 279 key = None 280 try: 281 try: 282 float(tokens[0]) 283 parse = float if "." in tokens[0] else int 284 keyvals[-1][1].extend(list(map(parse, tokens))) 285 except ValueError: 286 # We have a new key 287 key = tokens.pop(0) 288 parse = float if "." in tokens[0] else int 289 keyvals.append((key, list(map(parse, tokens)))) 290 except Exception as exc: 291 raise RuntimeError("Exception:\n%s\nwhile parsing ddb header line:\n%s" % 292 (str(exc), line)) 293 294 # add the potential information 295 for line in self: 296 if "Database of total energy derivatives" in line: 297 break 298 header_lines.append(line.rstrip()) 299 300 h = AttrDict(version=version, lines=header_lines) 301 for key, value in keyvals: 302 if len(value) == 1: value = value[0] 303 h[key] = value 304 305 # Convert to array. Note that znucl is converted into integer 306 # to avoid problems with pymatgen routines that expect integer Z 307 # This of course will break any code for alchemical mixing. 308 arrays = { 309 "acell": dict(shape=(3, ), dtype=float), 310 "amu": dict(shape=(h.ntypat, ), dtype=float), 311 "kpt": dict(shape=(h.nkpt, 3), dtype=float), 312 "ngfft": dict(shape=(3, ), dtype=int), 313 # This is problematic because not all occupation factors are written 314 #"occ": dict(shape=(h.nsppol, h.nkpt, h.nband), dtype=float), 315 "rprim": dict(shape=(3, 3), dtype=float), 316 "spinat": dict(shape=(h.natom, 3), dtype=float), 317 "symrel": dict(shape=(h.nsym, 3, 3), dtype=int), 318 "tnons": dict(shape=(h.nsym, 3), dtype=float), 319 "xred": dict(shape=(h.natom, 3), dtype=float), 320 # In principle these two quantities are double but here we convert to int 321 # Alchemical mixing is therefore ignored. 322 "znucl": dict(shape=(h.ntypat,), dtype=int), 323 "zion": dict(shape=(h.ntypat,), dtype=int), 324 "symafm": dict(shape=(h.nsym,), dtype=int), 325 "wtk": dict(shape=(h.nkpt,), dtype=float), 326 } 327 328 for k, ainfo in arrays.items(): 329 try: 330 h[k] = np.reshape(np.array(h[k], dtype=ainfo["dtype"]), ainfo["shape"]) 331 except Exception as exc: 332 print("While Trying to reshape", k) 333 raise exc 334 335 # Transpose symrel because Abinit write matrices by columns. 336 h.symrel = np.array([s.T for s in h.symrel]) 337 338 return h 339 340 def _read_qpoints(self): 341 """Read the list q-points from the DDB file. Returns |numpy-array|.""" 342 # 2nd derivatives (non-stat.) - # elements : 36 343 # qpt 2.50000000E-01 0.00000000E+00 0.00000000E+00 1.0 344 345 # Since there are multiple occurrences of qpt in the DDB file 346 # we use seen to remove duplicates. 347 self.seek(0) 348 tokens, seen = [], set() 349 350 for line in self: 351 line = line.strip() 352 if line.startswith("qpt") and line not in seen: 353 seen.add(line) 354 tokens.append(line.replace("qpt", "")) 355 356 qpoints, weights = [], [] 357 for tok in tokens: 358 nums = list(map(float, tok.split())) 359 qpoints.append(nums[:3]) 360 weights.append(nums[3]) 361 362 return np.reshape(qpoints, (-1, 3)) 363 364 @lazy_property 365 def computed_dynmat(self): 366 """ 367 OrderedDict mapping q-point object to --> pandas Dataframe. 368 The |pandas-DataFrame| contains the columns: "idir1", "ipert1", "idir2", "ipert2", "cvalue" 369 and (idir1, ipert1, idir2, ipert2) as index. 370 371 .. note:: 372 373 The indices follow the Abinit (Fortran) notation so they start at 1. 374 """ 375 # TODO: Create mapping [(idir1, ipert1), (idir2, ipert2)] --> element 376 df_columns = "idir1 ipert1 idir2 ipert2 cvalue".split() 377 378 dynmat = OrderedDict() 379 for block in self.blocks: 380 # skip the blocks that are not related to second order derivatives 381 first_line = block["data"][0].strip() 382 if not first_line.startswith("2nd derivatives"): 383 continue 384 385 # Build q-point object. 386 qpt = Kpoint(frac_coords=block["qpt"], lattice=self.structure.reciprocal_lattice, weight=None, name=None) 387 388 # Build pandas dataframe with df_columns and (idir1, ipert1, idir2, ipert2) as index. 389 # Each line in data represents an element of the dynamical matric 390 # idir1 ipert1 idir2 ipert2 re_D im_D 391 df_rows, df_index = [], [] 392 for line in block["data"]: 393 line = line.strip() 394 if line.startswith("2nd derivatives") or line.startswith("qpt"): 395 continue 396 try: 397 toks = line.split() 398 idir1, ipert1 = p1 = (int(toks[0]), int(toks[1])) 399 idir2, ipert2 = p2 = (int(toks[2]), int(toks[3])) 400 toks[4] = toks[4].replace("D", "E") 401 toks[5] = toks[5].replace("D", "E") 402 cvalue = float(toks[4]) + 1j*float(toks[5]) 403 except Exception as exc: 404 cprint("exception while parsing line: %s" % line, "red") 405 raise exc 406 407 df_index.append(p1 + p2) 408 df_rows.append(dict(idir1=idir1, ipert1=ipert1, idir2=idir2, ipert2=ipert2, cvalue=cvalue)) 409 410 dynmat[qpt] = pd.DataFrame(df_rows, index=df_index, columns=df_columns) 411 412 return dynmat 413 414 @lazy_property 415 def blocks(self): 416 """ 417 DDB blocks. List of dictionaries, Each dictionary contains the following keys. 418 "qpt" with the reduced coordinates of the q-point. 419 "data" that is a list of strings with the entries of the dynamical matrix for this q-point. 420 """ 421 return self._read_blocks() 422 423 def _read_blocks(self): 424 # skip until the beginning of the db 425 self.seek(0) 426 while "Number of data blocks" not in self._file.readline(): 427 pass 428 429 blocks = [] 430 block_lines = [] 431 qpt = None 432 dord = None 433 qpt3 = None 434 435 for line in self: 436 # skip empty lines 437 if line.isspace(): 438 continue 439 440 if "List of bloks and their characteristics" in line: 441 # add last block when we reach the last part of the file. 442 # This line is present only if DDB has been produced by mrgddb 443 if block_lines: 444 blocks.append({"data": block_lines, "qpt": qpt, "qpt3": qpt3, "dord": dord}) 445 block_lines = [] 446 qpt = None 447 qpt3 = None 448 break 449 450 # Don't use lstring because we may reuse block_lines to write new DDB. 451 line = line.rstrip() 452 453 # new block --> detect order 454 if "# elements" in line: 455 if block_lines: 456 blocks.append({"data": block_lines, "qpt": qpt, "qpt3": qpt3, "dord": dord}) 457 458 tokens = line.split() 459 num_elements = int(tokens[-1]) 460 s = " ".join(tokens[:2]) 461 dord = {"Total energy": 0, 462 "1st derivatives": 1, 463 "2nd derivatives": 2, 464 "3rd derivatives": 3}.get(s, None) 465 if dord is None: 466 raise RuntimeError("Cannot detect derivative order from string: `%s`" % s) 467 468 block_lines = [] 469 qpt = None 470 qpt3 = None 471 472 block_lines.append(line) 473 if dord == 2 and "qpt" in line: 474 qpt = list(map(float, line.split()[1:4])) 475 476 # if needed accumulate the qpt coords in qpt3. stop when they reach 3. 477 if dord == 3: 478 if "qpt" in line: 479 qpt3 = [list(map(float, line.split()[1:4]))] 480 elif qpt3 and len(qpt3) < 3: 481 qpt3.append(list(map(float, line.split()[:3]))) 482 483 if block_lines: 484 blocks.append({"data": block_lines, "qpt": qpt, "qpt3": qpt3, "dord": dord}) 485 486 return blocks 487 488 @property 489 def qpoints(self): 490 """|KpointList| object with the list of q-points in reduced coordinates.""" 491 return self._qpoints 492 493 def has_qpoint(self, qpoint): 494 """True if the DDB file contains this q-point.""" 495 #qpoint = Kpoint.as_kpoint(qpoint, self.structure.reciprocal_lattice) 496 try: 497 self.qpoints.index(qpoint) 498 return True 499 except ValueError: 500 return False 501 502 def qindex(self, qpoint): 503 """ 504 The index of the q-point in the internal list of q-points. 505 Accepts: |Kpoint| instance or integer. 506 """ 507 if duck.is_intlike(qpoint): 508 return int(qpoint) 509 else: 510 return self.qpoints.index(qpoint) 511 512 @lazy_property 513 def guessed_ngqpt(self): 514 """ 515 Guess for the q-mesh divisions (ngqpt) inferred from the list of 516 q-points found in the DDB file. 517 518 .. warning:: 519 520 The mesh may not be correct if the DDB file contains points belonging 521 to different meshes and/or the Q-mesh is shifted. 522 """ 523 return self._guess_ngqpt() 524 525 def _guess_ngqpt(self): 526 """ 527 This function tries to figure out the value of ngqpt from the list of 528 points reported in the DDB file. 529 """ 530 if not self.qpoints: return None 531 # Build the union of the stars of the q-points. 532 all_qpoints = np.empty((len(self.qpoints) * len(self.structure.abi_spacegroup), 3)) 533 count = 0 534 for qpoint in self.qpoints: 535 for op in self.structure.abi_spacegroup: 536 all_qpoints[count] = op.rotate_k(qpoint.frac_coords, wrap_tows=False) 537 count += 1 538 539 # Replace zeros with np.inf 540 for q in all_qpoints: 541 q[q == 0] = np.inf 542 543 # Compute the minimum of the fractional coordinates along the 3 directions and invert 544 smalls = np.abs(all_qpoints).min(axis=0) 545 smalls[smalls == 0] = 1 546 ngqpt = np.rint(1 / smalls) 547 ngqpt[ngqpt == 0] = 1 548 549 return np.array(ngqpt, dtype=int) 550 551 @lazy_property 552 def params(self): 553 """:class:`OrderedDict` with parameters that might be subject to convergence studies.""" 554 names = ("nkpt", "nsppol", "ecut", "tsmear", "occopt", "ixc", "nband", "usepaw") 555 od = OrderedDict() 556 for k in names: 557 od[k] = self.header[k] 558 return od 559 560 def _add_params(self, obj): 561 """Add params (meta variable) to object ``obj``. Usually a phonon bands or phonon dos object.""" 562 if not hasattr(obj, "params"): 563 raise TypeError("object %s does not have `params` attribute" % type(obj)) 564 obj.params.update(self.params) 565 566 @lazy_property 567 def total_energy(self): 568 """ 569 Total energy in eV. None if not available. 570 """ 571 for block in self.blocks: 572 if block["dord"] == 0: 573 ene_ha = float(block["data"][1].split()[0].replace("D", "E")) 574 return Energy(ene_ha, "Ha").to("eV") 575 return None 576 577 @lazy_property 578 def cart_forces(self): 579 """ 580 Cartesian forces in eV / Ang 581 None if not available i.e. if the GS DDB has not been merged. 582 """ 583 for block in self.blocks: 584 if block["dord"] != 1: continue 585 natom = len(self.structure) 586 fred = np.empty((natom, 3)) 587 for line in block["data"][1:]: 588 idir, ipert, fval = line.split()[:3] 589 # F --> C 590 idir, ipert = int(idir) - 1, int(ipert) - 1 591 if ipert < natom: 592 fred[ipert, idir] = float(fval.replace("D", "E")) 593 594 # Fred stores d(etotal)/d(xred) 595 # this array has *not* been corrected by enforcing 596 # the translational symmetry, namely that the sum of force 597 # on all atoms is not necessarly zero. 598 # Compute fcart using same code as in fred2fcart. 599 # Note conversion to cartesian coordinates (bohr) AND 600 # negation to make a force out of a gradient. 601 gprimd = self.structure.reciprocal_lattice.matrix / (2 * np.pi) * abu.Bohr_Ang 602 #fcart = - np.matmul(fred, gprimd) 603 fcart = - np.matmul(fred, gprimd.T) 604 # Subtract off average force from each force component 605 favg = fcart.sum(axis=0) / len(self.structure) 606 fcart -= favg 607 608 return fcart * abu.Ha_eV / abu.Bohr_Ang 609 610 return None 611 612 @lazy_property 613 def cart_stress_tensor(self): 614 """ 615 |Stress| tensor in cartesian coordinates (GPa units). None if not available. 616 """ 617 for block in self.blocks: 618 if block["dord"] != 1: continue 619 svoigt = np.empty(6) 620 # Abinit stress is in cart coords and Ha/Bohr**3 621 # Map (idir, ipert) --> voigt 622 uniax, shear = len(self.structure) + 3, len(self.structure) + 4 623 dirper2voigt = { 624 (1, uniax): 0, 625 (2, uniax): 1, 626 (3, uniax): 2, 627 (1, shear): 3, 628 (2, shear): 4, 629 (3, shear): 5} 630 631 for line in block["data"][1:]: 632 idir, ipert, fval = line.split()[:3] 633 idp = int(idir), int(ipert) 634 if idp in dirper2voigt: 635 svoigt[dirper2voigt[idp]] = float(fval.replace("D", "E")) 636 637 # Convert from Ha/Bohr^3 to GPa 638 return Stress.from_voigt(svoigt * abu.HaBohr3_GPa) 639 640 return None 641 642 def has_lo_to_data(self, select="at_least_one"): 643 """ 644 True if the DDB file contains the data required to compute the LO-TO splitting. 645 """ 646 return self.has_epsinf_terms(select=select) and self.has_bec_terms(select=select) 647 648 @lru_cache(typed=True) 649 def has_at_least_one_atomic_perturbation(self, qpt=None): 650 """ 651 True if the DDB file contains info on (at least one) atomic perturbation. 652 If the coordinates of a q point are provided only the specified qpt will be considered. 653 """ 654 natom = len(self.structure) 655 ap_list = list(itertools.product(range(1, 4), range(1, natom + 1))) 656 657 for qpt_dm, df in self.computed_dynmat.items(): 658 if qpt is not None and qpt_dm != qpt: continue 659 660 index_set = set(df.index) 661 for p1 in ap_list: 662 for p2 in ap_list: 663 p12 = p1 + p2 664 if p12 in index_set: return True 665 666 return False 667 668 @lru_cache(typed=True) 669 def has_epsinf_terms(self, select="at_least_one"): 670 """ 671 True if the DDB file contains info on the electric-field perturbation. 672 673 Args: 674 select: Possible values in ["at_least_one", "at_least_one_diagoterm", "all"] 675 If select == "at_least_one", we check if there's at least one entry 676 associated to the electric field and we assume that anaddb will be able 677 to reconstruct the full tensor by symmetry. 678 "at_least_one_diagoterm" is similar but it only checks for the presence of one diagonal term. 679 If select == "all", all tensor components must be present in the DDB file. 680 """ 681 gamma = Kpoint.gamma(self.structure.reciprocal_lattice) 682 if gamma not in self.computed_dynmat: 683 return False 684 685 index_set = set(self.computed_dynmat[gamma].index) 686 687 natom = len(self.structure) 688 ep_list = list(itertools.product(range(1, 4), [natom + 2])) 689 for p1 in ep_list: 690 for p2 in ep_list: 691 p12 = p1 + p2 692 p21 = p2 + p1 693 if select == "at_least_one": 694 if p12 in index_set: return True 695 elif select == "at_least_one_diagoterm": 696 if p12 == p21 and p12 in index_set: 697 return True 698 elif select == "all": 699 if p12 not in index_set and p21 not in index_set: 700 return False 701 else: 702 raise ValueError("Wrong select %s" % str(select)) 703 704 return False if select in ("at_least_one", "at_least_one_diagoterm") else True 705 706 @deprecated(message="has_emacro_terms is deprecated and will be removed in abipy 0.8, use has_epsinf_terms") 707 def has_emacro_terms(self, **kwargs): 708 return self.has_epsinf_terms(**kwargs) 709 710 @lru_cache(typed=True) 711 def has_bec_terms(self, select="at_least_one"): 712 """ 713 True if the DDB file contains info on the Born effective charges. 714 715 Args: 716 select: Possible values in ["at_least_one", "all"] 717 By default, we check if there's at least one entry associated to atomic displacement 718 and electric field and we assume that anaddb will be able to reconstruct the full tensor by symmetry. 719 If select == "all", all bec components must be present in the DDB file. 720 """ 721 gamma = Kpoint.gamma(self.structure.reciprocal_lattice) 722 if gamma not in self.computed_dynmat: 723 return False 724 index_set = set(self.computed_dynmat[gamma].index) 725 natom = len(self.structure) 726 ep_list = list(itertools.product(range(1, 4), [natom + 2])) 727 ap_list = list(itertools.product(range(1, 4), range(1, natom + 1))) 728 729 # helper function to get the value, since has a cumbersome notation 730 # when dealing with tuples as indices 731 dmg = self.computed_dynmat[gamma] 732 733 def non_zero_value(ind): 734 if ind not in index_set: 735 return False 736 v = dmg.loc[[ind]].iloc[0].cvalue 737 return v != 0.0 738 739 # if the BECs perturbations are not calculated, abinit can set all 740 # of them to 0 and writes them in the DDB. To be considered 741 # as present at lest one should be different from zero. 742 not_all_zero = False 743 744 for ap1 in ap_list: 745 for ep2 in ep_list: 746 p12 = ap1 + ep2 747 p21 = ep2 + ap1 748 if select == "at_least_one": 749 if (p12 in index_set and non_zero_value(p12)) or \ 750 (p21 in index_set and non_zero_value(p21)): 751 return True 752 elif select == "all": 753 if p12 not in index_set and p21 not in index_set: 754 return False 755 not_all_zero = not_all_zero or non_zero_value(p12) or non_zero_value(p21) 756 else: 757 raise ValueError("Wrong select %s" % str(select)) 758 759 return False if select == "at_least_one" else not_all_zero 760 761 @lru_cache(typed=True) 762 def has_strain_terms(self, select="all"): 763 """ 764 True if the DDB file contains info on the (clamped-ion) strain perturbation 765 (i.e. 2nd order derivatives wrt strain) 766 767 Args: 768 select: Possible values in ["at_least_one", "all"] 769 If select == "at_least_one", we check if there's at least one entry associated to the strain. 770 and we assume that anaddb will be able to reconstruct the full tensor by symmetry. 771 If select == "all", all tensor components must be present in the DDB file. 772 773 .. note:: 774 775 As anaddb is not yet able to reconstruct the strain terms by symmetry, 776 the default value for select is "all" 777 """ 778 gamma = Kpoint.gamma(self.structure.reciprocal_lattice) 779 if gamma not in self.computed_dynmat: 780 return False 781 782 index_set = set(self.computed_dynmat[gamma].index) 783 784 natom = len(self.structure) 785 sp_list = list(itertools.product(range(1, 4), [natom + 3, natom + 4])) 786 for p1 in sp_list: 787 for p2 in sp_list: 788 p12 = p1 + p2 789 p21 = p2 + p1 790 if select == "at_least_one": 791 if p12 in index_set: return True 792 elif select == "all": 793 if p12 not in index_set and p21 not in index_set: 794 #print("p12", p12, "not in index_set") 795 return False 796 else: 797 raise ValueError("Wrong select %s" % str(select)) 798 799 return False if select == "at_least_one" else True 800 801 @lru_cache(typed=True) 802 def has_internalstrain_terms(self, select="all"): 803 """ 804 True if the DDB file contains internal strain terms 805 i.e "off-diagonal" 2nd order derivatives wrt (strain, atomic displacement) 806 807 Args: 808 select: Possible values in ["at_least_one", "all"] 809 If select == "at_least_one", we check if there's at least one entry associated to the strain. 810 and we assume that anaddb will be able to reconstruct the full tensor by symmetry. 811 If select == "all", all tensor components must be present in the DDB file. 812 813 .. note:: 814 815 As anaddb is not yet able to reconstruct the strain terms by symmetry, 816 the default value for select is "all" 817 """ 818 gamma = Kpoint.gamma(self.structure.reciprocal_lattice) 819 if gamma not in self.computed_dynmat: 820 return False 821 822 index_set = set(self.computed_dynmat[gamma].index) 823 824 natom = len(self.structure) 825 sp_list = list(itertools.product(range(1, 4), [natom + 3, natom + 4])) 826 ap_list = list(itertools.product(range(1, 4), range(1, natom + 1))) 827 for p1 in sp_list: 828 for p2 in ap_list: 829 p12 = p1 + p2 830 p21 = p2 + p1 831 if select == "at_least_one": 832 if p12 in index_set: return True 833 elif select == "all": 834 if p12 not in index_set and p21 not in index_set: 835 #print("p12", p12, "non in index") 836 return False 837 else: 838 raise ValueError("Wrong select %s" % str(select)) 839 840 return False if select == "at_least_one" else True 841 842 @lru_cache(typed=True) 843 def has_piezoelectric_terms(self, select="all"): 844 """ 845 True if the DDB file contains piezoelectric terms 846 i.e "off-diagonal" 2nd order derivatives wrt (electric_field, strain) 847 848 Args: 849 select: Possible values in ["at_least_one", "all"] 850 If select == "at_least_one", we check if there's at least one entry associated to the strain. 851 and we assume that anaddb will be able to reconstruct the full tensor by symmetry. 852 If select == "all", all tensor components must be present in the DDB file. 853 854 .. note:: 855 856 As anaddb is not yet able to reconstruct the (strain, electric) terms by symmetry, 857 the default value for select is "all" 858 """ 859 gamma = Kpoint.gamma(self.structure.reciprocal_lattice) 860 if gamma not in self.computed_dynmat: 861 return False 862 863 index_set = set(self.computed_dynmat[gamma].index) 864 865 natom = len(self.structure) 866 sp_list = list(itertools.product(range(1, 4), [natom + 3, natom + 4])) 867 ep_list = list(itertools.product(range(1, 4), [natom + 2])) 868 for p1 in sp_list: 869 for p2 in ep_list: 870 p12 = p1 + p2 871 p21 = p2 + p1 872 if select == "at_least_one": 873 if p12 in index_set: return True 874 elif select == "all": 875 if p12 not in index_set and p21 not in index_set: return False 876 else: 877 raise ValueError("Wrong select %s" % str(select)) 878 879 return False if select == "at_least_one" else True 880 881 def view_phononwebsite(self, browser=None, verbose=0, dryrun=False, **kwargs): 882 """ 883 Invoke anaddb to compute phonon bands. 884 Produce JSON file that can be parsed from the phononwebsite_ and open it in ``browser``. 885 886 Args: 887 browser: Open webpage in ``browser``. Use default $BROWSER if None. 888 verbose: Verbosity level 889 dryrun: Activate dryrun mode for unit testing purposes. 890 kwargs: Passed to ``anaget_phbst_and_phdos_files``. 891 892 Return: Exit status 893 """ 894 # Call anaddb to get phonon bands. 895 if "nqsmall" not in kwargs: kwargs["nqsmall"] = 0 896 phbst_file, phdos_file = self.anaget_phbst_and_phdos_files(**kwargs) 897 phbands = phbst_file.phbands 898 phbst_file.close() 899 900 return phbands.view_phononwebsite(browser=browser, verbose=verbose, dryrun=dryrun) 901 902 def yield_figs(self, **kwargs): # pragma: no cover 903 """ 904 This function *generates* a predefined list of matplotlib figures with minimal input from the user. 905 """ 906 yield self.structure.plot(show=False) 907 yield self.qpoints.plot(show=False) 908 yield self.structure.plot_bz(show=False) 909 910 def anaget_phmodes_at_qpoint(self, qpoint=None, asr=2, chneut=1, dipdip=1, workdir=None, mpi_procs=1, 911 manager=None, verbose=0, lo_to_splitting=False, spell_check=True, 912 directions=None, anaddb_kwargs=None, return_input=False): 913 """ 914 Execute anaddb to compute phonon modes at the given q-point. Non analytical contribution 915 can be added if qpoint is Gamma and required elements are present in the DDB. 916 917 Args: 918 qpoint: Reduced coordinates of the qpoint where phonon modes are computed. 919 asr, chneut, dipdip: Anaddb input variable. See official documentation. 920 workdir: Working directory. If None, a temporary directory is created. 921 mpi_procs: Number of MPI processes to use. 922 manager: |TaskManager| object. If None, the object is initialized from the configuration file 923 verbose: verbosity level. Set it to a value > 0 to get more information. 924 lo_to_splitting: Allowed values are [True, False, "automatic"]. Defaults to False 925 If True the LO-TO splitting will be calculated if qpoint == Gamma and the non_anal_directions 926 non_anal_phfreqs attributes will be addeded to the phonon band structure. 927 "automatic" activates LO-TO if the DDB file contains the dielectric tensor and Born effective charges. 928 directions: list of 3D directions along which the LO-TO splitting will be calculated. If None the three 929 cartesian direction will be used. 930 anaddb_kwargs: additional kwargs for anaddb. 931 return_input: True if |AnaddbInput| object should be returned as 2nd argument 932 933 Return: |PhononBands| object. 934 """ 935 if qpoint is None: 936 qpoint = self.qpoints[0] 937 if len(self.qpoints) != 1: 938 raise ValueError("%s contains %s qpoints and the choice is ambiguous.\n" 939 "Please specify the qpoint." % (self, len(self.qpoints))) 940 941 return self.anaget_phmodes_at_qpoints(qpoints=[qpoint], asr=asr, chneut=chneut, dipdip=dipdip, 942 workdir=workdir, mpi_procs=mpi_procs, manager=manager, 943 verbose=verbose, lo_to_splitting=lo_to_splitting, spell_check=spell_check, 944 directions=directions, anaddb_kwargs=anaddb_kwargs, return_input=return_input) 945 946 def anaget_phmodes_at_qpoints(self, qpoints=None, asr=2, chneut=1, dipdip=1, ifcflag=0, ngqpt=None, 947 workdir=None, mpi_procs=1, manager=None, verbose=0, lo_to_splitting=False, 948 spell_check=True, directions=None, anaddb_kwargs=None, return_input=False): 949 """ 950 Execute anaddb to compute phonon modes at the given list of q-points. Non analytical contribution 951 can be added if Gamma belongs to the list and required elements are present in the DDB. 952 If the list of q-points contains points that are not present in the DDB the values will be 953 interpolated and ifcflag should be set to 1. 954 955 Args: 956 qpoints: Reduced coordinates of a list of qpoints where phonon modes are computed. 957 asr, chneut, dipdip, ifcflag: Anaddb input variable. See official documentation. 958 ngqpt: Number of divisions for the q-mesh in the DDB file. Auto-detected if None (default). 959 workdir: Working directory. If None, a temporary directory is created. 960 mpi_procs: Number of MPI processes to use. 961 manager: |TaskManager| object. If None, the object is initialized from the configuration file 962 verbose: verbosity level. Set it to a value > 0 to get more information. 963 lo_to_splitting: Allowed values are [True, False, "automatic"]. Defaults to False 964 If True the LO-TO splitting will be calculated if qpoint == Gamma and the non_anal_directions 965 non_anal_phfreqs attributes will be addeded to the phonon band structure. 966 "automatic" activates LO-TO if the DDB file contains the dielectric tensor and Born effective charges. 967 directions: list of 3D directions along which the LO-TO splitting will be calculated. If None the three 968 cartesian direction will be used. 969 anaddb_kwargs: additional kwargs for anaddb. 970 return_input: True if |AnaddbInput| object should be returned as 2nd argument 971 972 Return: |PhononBands| object. 973 """ 974 if qpoints is None: 975 qpoints = self.qpoints 976 977 if ifcflag == 0: 978 try: 979 iqs = [self.qindex(q) for q in qpoints] 980 except Exception: 981 raise ValueError("input qpoint %s not in %s.\nddb.qpoints:\n%s" % ( 982 qpoints, self.filepath, self.qpoints)) 983 984 qpoints = [self.qpoints[iq] for iq in iqs] 985 else: 986 rl = self.structure.lattice.reciprocal_lattice 987 qpoints = [Kpoint(qc, rl) for qc in qpoints] 988 if ngqpt is None: 989 ngqpt = self.guessed_ngqpt 990 991 gamma = None 992 for q in qpoints: 993 if q.is_gamma(): 994 gamma = q 995 996 if lo_to_splitting == "automatic": 997 lo_to_splitting = self.has_lo_to_data() and gamma and dipdip != 0 998 999 # if lo_to_splitting and gamma and not self.has_lo_to_data(): 1000 # cprint("lo_to_splitting set to True but Eps_inf and Becs are not available in DDB %s:" % self.filepath) 1001 1002 inp = AnaddbInput.modes_at_qpoints(self.structure, qpoints, asr=asr, chneut=chneut, dipdip=dipdip, 1003 ifcflag=ifcflag, ngqpt=ngqpt, lo_to_splitting=lo_to_splitting, directions=directions, 1004 anaddb_kwargs=anaddb_kwargs, spell_check=spell_check) 1005 1006 task = self._run_anaddb_task(inp, mpi_procs, workdir, manager, verbose) 1007 1008 with task.open_phbst() as ncfile: 1009 if lo_to_splitting and gamma: 1010 anaddbnc_path = task.outpath_from_ext("anaddb.nc") 1011 ncfile.phbands.read_non_anal_from_file(anaddbnc_path) 1012 1013 print("Calculation completed.\nAnaddb results available in dir:", task.workdir) 1014 return ncfile.phbands if not return_input else (ncfile.phbands, inp) 1015 1016 def anaget_phbst_and_phdos_files(self, nqsmall=10, qppa=None, ndivsm=20, line_density=None, asr=2, chneut=1, dipdip=1, 1017 dos_method="tetra", lo_to_splitting="automatic", ngqpt=None, qptbounds=None, 1018 anaddb_kwargs=None, verbose=0, spell_check=True, 1019 mpi_procs=1, workdir=None, manager=None, return_input=False): 1020 """ 1021 Execute anaddb to compute the phonon band structure and the phonon DOS. 1022 Return contex manager that closes the files automatically. 1023 1024 .. important:: 1025 1026 Use: 1027 1028 with ddb.anaget_phbst_and_phdos_files(...) as g: 1029 phbst_file, phdos_file = g[0], g[0] 1030 1031 to ensure the netcdf files are closed instead of: 1032 1033 phbst_file, phdos_file = ddb.anaget_phbst_and_phdos_files(...) 1034 1035 Args: 1036 nqsmall: Defines the homogeneous q-mesh used for the DOS. Gives the number of divisions 1037 used to sample the smallest lattice vector. If 0, DOS is not computed and 1038 (phbst, None) is returned. 1039 qppa: Defines the homogeneous q-mesh used for the DOS in units of q-points per reciproval atom. 1040 Overrides nqsmall. 1041 ndivsm: Number of division used for the smallest segment of the q-path. 1042 line_density: Defines the a density of k-points per reciprocal atom to plot the phonon dispersion. 1043 Overrides ndivsm. 1044 asr, chneut, dipdip: Anaddb input variable. See official documentation. 1045 dos_method: Technique for DOS computation in Possible choices: "tetra", "gaussian" or "gaussian:0.001 eV". 1046 In the later case, the value 0.001 eV is used as gaussian broadening. 1047 lo_to_splitting: Allowed values are [True, False, "automatic"]. Defaults to "automatic" 1048 If True the LO-TO splitting will be calculated and the non_anal_directions 1049 and the non_anal_phfreqs attributes will be addeded to the phonon band structure. 1050 "automatic" activates LO-TO if the DDB file contains the dielectric tensor and Born effective charges. 1051 ngqpt: Number of divisions for the q-mesh in the DDB file. Auto-detected if None (default). 1052 qptbounds: Boundaries of the path. If None, the path is generated from an internal database 1053 depending on the input structure. 1054 anaddb_kwargs: additional kwargs for anaddb. 1055 verbose: verbosity level. Set it to a value > 0 to get more information. 1056 mpi_procs: Number of MPI processes to use. 1057 workdir: Working directory. If None, a temporary directory is created. 1058 manager: |TaskManager| object. If None, the object is initialized from the configuration file. 1059 return_input: True if |AnaddbInput| object should be attached to the Context manager. 1060 1061 Returns: Context manager with two files: 1062 |PhbstFile| with the phonon band structure. 1063 |PhdosFile| with the the phonon DOS. 1064 """ 1065 if ngqpt is None: ngqpt = self.guessed_ngqpt 1066 1067 if lo_to_splitting == "automatic": 1068 lo_to_splitting = self.has_lo_to_data() and dipdip != 0 1069 1070 if lo_to_splitting and not self.has_lo_to_data(): 1071 cprint("lo_to_splitting is True but Eps_inf and Becs are not available in DDB: %s" % self.filepath, "yellow") 1072 1073 inp = AnaddbInput.phbands_and_dos( 1074 self.structure, ngqpt=ngqpt, ndivsm=ndivsm, line_density=line_density, 1075 nqsmall=nqsmall, qppa=qppa, q1shft=(0, 0, 0), qptbounds=qptbounds, 1076 asr=asr, chneut=chneut, dipdip=dipdip, dos_method=dos_method, lo_to_splitting=lo_to_splitting, 1077 anaddb_kwargs=anaddb_kwargs, spell_check=spell_check) 1078 1079 task = self._run_anaddb_task(inp, mpi_procs, workdir, manager, verbose) 1080 1081 # Use ExitStackWithFiles so that caller can use with contex manager. 1082 exit_stack = ExitStackWithFiles() 1083 1084 # Open file and add metadata to phbands from DDB 1085 # TODO: in principle phbands.add_params? 1086 phbst_file = task.open_phbst() 1087 exit_stack.enter_context(phbst_file) 1088 1089 self._add_params(phbst_file.phbands) 1090 if lo_to_splitting: 1091 anaddbnc_path = task.outpath_from_ext("anaddb.nc") 1092 phbst_file.phbands.read_non_anal_from_file(anaddbnc_path) 1093 1094 phdos_file = None 1095 if inp["prtdos"] != 0: 1096 phdos_file = task.open_phdos() 1097 #self._add_params(phdos_file.phdos) 1098 1099 exit_stack.enter_context(phdos_file) 1100 if return_input: exit_stack.input = inp 1101 1102 return exit_stack 1103 1104 def get_coarse(self, ngqpt_coarse, filepath=None): 1105 """ 1106 Get a version of this file on a coarse mesh 1107 1108 Args: 1109 ngqpt_coarse: list of ngqpt indexes that must be a sub-mesh of the original ngqpt 1110 filepath: Filename for coarse DDB. If None, temporary filename is used. 1111 1112 Return: |DdbFile| on coarse mesh. 1113 """ 1114 # Check if ngqpt is a sub-mesh of ngqpt 1115 ngqpt_fine = self.guessed_ngqpt 1116 if any([a % b for a, b in zip(ngqpt_fine, ngqpt_coarse)]): 1117 raise ValueError('Coarse q-mesh is not a sub-mesh of the current q-mesh') 1118 1119 # Get the points in the fine mesh 1120 fine_qpoints = [q.frac_coords for q in self.qpoints] 1121 1122 # Generate the points of the coarse mesh 1123 map_fine_to_coarse = [] 1124 nx,ny,nz = ngqpt_coarse 1125 for i,j,k in itertools.product(range(-int(nx/2), int(nx/2) + 1), 1126 range(-int(ny/2), int(ny/2) + 1), 1127 range(-int(nz/2), int(nz/2) + 1)): 1128 coarse_qpt = np.array([i, j, k]) / np.array(ngqpt_coarse) 1129 for n,fine_qpt in enumerate(fine_qpoints): 1130 if np.allclose(coarse_qpt, fine_qpt): 1131 map_fine_to_coarse.append(n) 1132 1133 # Write the file with a subset of q-points 1134 if filepath is None: 1135 _, filepath = tempfile.mkstemp(suffix="_DDB", text=True) 1136 1137 self.write(filepath, filter_blocks=map_fine_to_coarse) 1138 return self.__class__(filepath) 1139 1140 def anacompare_asr(self, asr_list=(0, 2), chneut_list=(1,), dipdip=1, lo_to_splitting="automatic", 1141 nqsmall=10, ndivsm=20, dos_method="tetra", ngqpt=None, 1142 verbose=0, mpi_procs=1): 1143 """ 1144 Invoke anaddb to compute the phonon band structure and the phonon DOS with different 1145 values of the ``asr`` input variable (acoustic sum rule treatment). 1146 Build and return |PhononBandsPlotter| object. 1147 1148 Args: 1149 asr_list: List of ``asr`` values to test. 1150 chneut_list: List of ``chneut`` values to test (used by anaddb only if dipdip == 1). 1151 dipdip: 1 to activate treatment of dipole-dipole interaction (requires BECS and dielectric tensor). 1152 lo_to_splitting: Allowed values are [True, False, "automatic"]. Defaults to "automatic" 1153 If True the LO-TO splitting will be calculated if qpoint == Gamma and the non_anal_directions 1154 non_anal_phfreqs attributes will be addeded to the phonon band structure. 1155 "automatic" activates LO-TO if the DDB file contains the dielectric tensor and Born effective charges. 1156 nqsmall: Defines the q-mesh for the phonon DOS in terms of 1157 the number of divisions to be used to sample the smallest reciprocal lattice vector. 1158 0 to disable DOS computation. 1159 ndivsm: Number of division used for the smallest segment of the q-path 1160 dos_method: Technique for DOS computation in Possible choices: "tetra", "gaussian" or "gaussian:0.001 eV". 1161 In the later case, the value 0.001 eV is used as gaussian broadening 1162 ngqpt: Number of divisions for the ab-initio q-mesh in the DDB file. Auto-detected if None (default) 1163 verbose: Verbosity level. 1164 mpi_procs: Number of MPI processes used by anaddb. 1165 1166 Return: 1167 |PhononBandsPlotter| object. 1168 1169 Client code can use ``plotter.combiplot()`` or ``plotter.gridplot()`` to visualize the results. 1170 """ 1171 phbands_plotter = PhononBandsPlotter() 1172 1173 for asr, chneut in itertools.product(asr_list, chneut_list): 1174 phbst_file, phdos_file = self.anaget_phbst_and_phdos_files( 1175 nqsmall=nqsmall, ndivsm=ndivsm, asr=asr, chneut=chneut, dipdip=dipdip, dos_method=dos_method, 1176 lo_to_splitting=lo_to_splitting, ngqpt=ngqpt, qptbounds=None, 1177 anaddb_kwargs=None, verbose=verbose, mpi_procs=mpi_procs, workdir=None, manager=None) 1178 1179 label = "asr: %d, dipdip: %d, chneut: %d" % (asr, dipdip, chneut) 1180 if phdos_file is not None: 1181 phbands_plotter.add_phbands(label, phbst_file.phbands, phdos=phdos_file.phdos) 1182 phdos_file.close() 1183 else: 1184 phbands_plotter.add_phbands(label, phbst_file.phbands) 1185 phbst_file.close() 1186 1187 return phbands_plotter 1188 1189 def anacompare_dipdip(self, chneut_list=(1,), asr=2, lo_to_splitting="automatic", 1190 nqsmall=10, ndivsm=20, dos_method="tetra", ngqpt=None, 1191 verbose=0, mpi_procs=1): 1192 """ 1193 Invoke anaddb to compute the phonon band structure and the phonon DOS with different 1194 values of the ``dipdip`` input variable (dipole-dipole treatment). 1195 Build and return |PhononDosPlotter| object. 1196 1197 Args: 1198 chneut_list: List of ``chneut`` values to test (used for dipdip == 1). 1199 asr_list: Variable for acoustic sum rule treatment. 1200 lo_to_splitting: Allowed values are [True, False, "automatic"]. Defaults to "automatic" 1201 If True the LO-TO splitting will be calculated if qpoint == Gamma and the non_anal_directions 1202 non_anal_phfreqs attributes will be addeded to the phonon band structure. 1203 "automatic" activates LO-TO if the DDB file contains the dielectric tensor and Born effective charges. 1204 nqsmall: Defines the q-mesh for the phonon DOS in terms of 1205 the number of divisions to be used to sample the smallest reciprocal lattice vector. 1206 0 to disable DOS computation. 1207 ndivsm: Number of division used for the smallest segment of the q-path 1208 dos_method: Technique for DOS computation in Possible choices: "tetra", "gaussian" or "gaussian:0.001 eV". 1209 In the later case, the value 0.001 eV is used as gaussian broadening 1210 ngqpt: Number of divisions for the ab-initio q-mesh in the DDB file. Auto-detected if None (default) 1211 verbose: Verbosity level. 1212 mpi_procs: Number of MPI processes used by anaddb. 1213 1214 Return: 1215 |PhononDosPlotter| object. 1216 1217 Client code can use ``plotter.combiplot()`` or ``plotter.gridplot()`` to visualize the results. 1218 """ 1219 phbands_plotter = PhononBandsPlotter() 1220 1221 for dipdip in (0, 1): 1222 my_chneut_list = chneut_list if dipdip != 0 else [0] 1223 for chneut in my_chneut_list: 1224 phbst_file, phdos_file = self.anaget_phbst_and_phdos_files( 1225 nqsmall=nqsmall, ndivsm=ndivsm, asr=asr, chneut=chneut, dipdip=dipdip, dos_method=dos_method, 1226 lo_to_splitting=lo_to_splitting, ngqpt=ngqpt, qptbounds=None, 1227 anaddb_kwargs=None, verbose=verbose, mpi_procs=mpi_procs, workdir=None, manager=None) 1228 1229 label = "asr: %d, dipdip: %d, chneut: %d" % (asr, dipdip, chneut) 1230 if phdos_file is not None: 1231 phbands_plotter.add_phbands(label, phbst_file.phbands, phdos=phdos_file.phdos) 1232 phdos_file.close() 1233 else: 1234 phbands_plotter.add_phbands(label, phbst_file.phbands) 1235 phbst_file.close() 1236 1237 return phbands_plotter 1238 1239 def anacompare_phdos(self, nqsmalls, asr=2, chneut=1, dipdip=1, dos_method="tetra", ngqpt=None, 1240 verbose=0, num_cpus=1, stream=sys.stdout): 1241 """ 1242 Invoke Anaddb to compute Phonon DOS with different q-meshes. The ab-initio dynamical matrix 1243 reported in the DDB_ file will be Fourier-interpolated on the list of q-meshes specified 1244 by ``nqsmalls``. Useful to perform convergence studies. 1245 1246 Args: 1247 nqsmalls: List of integers defining the q-mesh for the DOS. Each integer gives 1248 the number of divisions to be used to sample the smallest reciprocal lattice vector. 1249 asr, chneut, dipdip: Anaddb input variable. See official documentation. 1250 dos_method: Technique for DOS computation in Possible choices: "tetra", "gaussian" or "gaussian:0.001 eV". 1251 In the later case, the value 0.001 eV is used as gaussian broadening 1252 ngqpt: Number of divisions for the ab-initio q-mesh in the DDB file. Auto-detected if None (default) 1253 verbose: Verbosity level. 1254 num_cpus: Number of CPUs (threads) used to parallellize the calculation of the DOSes. Autodetected if None. 1255 stream: File-like object used for printing. 1256 1257 Return: 1258 ``namedtuple`` with the following attributes:: 1259 1260 phdoses: List of |PhononDos| objects 1261 plotter: |PhononDosPlotter| object. 1262 Client code can use ``plotter.gridplot()`` to visualize the results. 1263 """ 1264 #num_cpus = get_ncpus() // 2 if num_cpus is None else num_cpus 1265 if num_cpus <= 0: num_cpus = 1 1266 num_cpus = min(num_cpus, len(nqsmalls)) 1267 1268 def do_work(nqsmall): 1269 phbst_file, phdos_file = self.anaget_phbst_and_phdos_files( 1270 nqsmall=nqsmall, ndivsm=1, asr=asr, chneut=chneut, dipdip=dipdip, dos_method=dos_method, ngqpt=ngqpt) 1271 phdos = phdos_file.phdos 1272 phbst_file.close() 1273 phdos_file.close() 1274 return phdos 1275 1276 if num_cpus == 1: 1277 # Sequential version 1278 phdoses = [do_work(nqs) for nqs in nqsmalls] 1279 1280 else: 1281 # Threads 1282 if verbose: 1283 print("Computing %d phonon DOS with %d threads" % (len(nqsmalls), num_cpus)) 1284 phdoses = [None] * len(nqsmalls) 1285 1286 def worker(): 1287 while True: 1288 nqsm, phdos_index = q.get() 1289 phdos = do_work(nqsm) 1290 phdoses[phdos_index] = phdos 1291 q.task_done() 1292 1293 from threading import Thread 1294 from queue import Queue 1295 1296 q = Queue() 1297 for i in range(num_cpus): 1298 t = Thread(target=worker) 1299 t.daemon = True 1300 t.start() 1301 1302 for i, nqsmall in enumerate(nqsmalls): 1303 q.put((nqsmall, i)) 1304 1305 # block until all tasks are done 1306 q.join() 1307 1308 # Compute relative difference wrt last phonon DOS. Be careful because the DOSes may be defined 1309 # on different frequency meshes ==> spline on the mesh of the last DOS. 1310 last_mesh, converged = phdoses[-1].mesh, False 1311 for i, phdos in enumerate(phdoses[:-1]): 1312 splined_dos = phdos.spline_on_mesh(last_mesh) 1313 abs_diff = (splined_dos - phdoses[-1]).abs() 1314 if verbose: 1315 print(" Delta(Phdos[%d] - Phdos[%d]) / Phdos[%d]: %f" % 1316 (i, len(phdoses)-1, len(phdoses)-1, abs_diff.integral().values[-1]), file=stream) 1317 1318 # Fill the plotter. 1319 plotter = PhononDosPlotter() 1320 for nqsmall, phdos in zip(nqsmalls, phdoses): 1321 plotter.add_phdos(label="nqsmall %d" % nqsmall, phdos=phdos) 1322 1323 return dict2namedtuple(phdoses=phdoses, plotter=plotter) 1324 1325 def anacompare_rifcsph(self, rifcsph_list, asr=2, chneut=1, dipdip=1, lo_to_splitting="automatic", 1326 ndivsm=20, ngqpt=None, verbose=0, mpi_procs=1): 1327 """ 1328 Invoke anaddb to compute the phonon band structure and the phonon DOS with different 1329 values of the ``asr`` input variable (acoustic sum rule treatment). 1330 Build and return |PhononBandsPlotter| object. 1331 1332 Args: 1333 rifcsph_list: List of rifcsph to analyze. 1334 asr, chneut, dipdip: Anaddb input variable. See official documentation. 1335 dipdip: 1 to activate treatment of dipole-dipole interaction (requires BECS and dielectric tensor). 1336 lo_to_splitting: Allowed values are [True, False, "automatic"]. Defaults to "automatic" 1337 If True the LO-TO splitting will be calculated if qpoint == Gamma and the non_anal_directions 1338 non_anal_phfreqs attributes will be addeded to the phonon band structure. 1339 "automatic" activates LO-TO if the DDB file contains the dielectric tensor and Born effective charges. 1340 ndivsm: Number of division used for the smallest segment of the q-path 1341 ngqpt: Number of divisions for the ab-initio q-mesh in the DDB file. Auto-detected if None (default) 1342 verbose: Verbosity level. 1343 mpi_procs: Number of MPI processes used by anaddb. 1344 1345 Return: 1346 |PhononBandsPlotter| object. 1347 1348 Client code can use ``plotter.combiplot()`` or ``plotter.gridplot()`` to visualize the results. 1349 """ 1350 phbands_plotter = PhononBandsPlotter() 1351 1352 for rifcsph in rifcsph_list: 1353 phbst_file, _ = self.anaget_phbst_and_phdos_files( 1354 nqsmall=0, ndivsm=ndivsm, asr=asr, chneut=chneut, dipdip=dipdip, dos_method="tetra", 1355 lo_to_splitting=lo_to_splitting, ngqpt=ngqpt, qptbounds=None, 1356 anaddb_kwargs={"rifcsph": rifcsph}, 1357 verbose=verbose, mpi_procs=mpi_procs, workdir=None, manager=None) 1358 1359 label = "rifcsph: %f" % rifcsph 1360 phbands_plotter.add_phbands(label, phbst_file.phbands) 1361 phbst_file.close() 1362 1363 return phbands_plotter 1364 1365 def anaget_epsinf_and_becs(self, chneut=1, mpi_procs=1, workdir=None, manager=None, verbose=0): 1366 """ 1367 Call anaddb to compute the macroscopic electronic dielectric tensor (e_inf) 1368 and the Born effective charges in Cartesian coordinates. 1369 1370 Args: 1371 chneut: Anaddb input variable. See official documentation. 1372 manager: |TaskManager| object. If None, the object is initialized from the configuration file 1373 mpi_procs: Number of MPI processes to use. 1374 verbose: verbosity level. Set it to a value > 0 to get more information 1375 1376 Return: ``namedtuple`` with the following attributes:: 1377 epsinf: |DielectricTensor| object. 1378 becs: Becs objects. 1379 """ 1380 if not self.has_lo_to_data(): 1381 cprint("Dielectric tensor and Becs are not available in DDB: %s" % self.filepath, "yellow") 1382 1383 inp = AnaddbInput(self.structure, anaddb_kwargs={"chneut": chneut}) 1384 1385 task = self._run_anaddb_task(inp, mpi_procs, workdir, manager, verbose) 1386 anaddbnc_path = task.outpath_from_ext("anaddb.nc") 1387 1388 # Read data from the netcdf output file produced by anaddb. 1389 with ETSF_Reader(anaddbnc_path) as r: 1390 epsinf = DielectricTensor(r.read_value("emacro_cart").T.copy()) 1391 structure = r.read_structure() 1392 becs = Becs(r.read_value("becs_cart"), structure, chneut=inp["chneut"], order="f") 1393 return dict2namedtuple(epsinf=epsinf, becs=becs) 1394 1395 @deprecated(message="anaget_emacro_and_becs is deprecated and will be removed in abipy 0.8, use anaget_epsinf_and_becs") 1396 def anaget_emacro_and_becs(self, **kwargs): 1397 r = self.anaget_epsinf_and_becs(**kwargs) 1398 return r.epsinf, r.becs 1399 1400 def anaget_ifc(self, ifcout=None, asr=2, chneut=1, dipdip=1, ngqpt=None, 1401 mpi_procs=1, workdir=None, manager=None, verbose=0, anaddb_kwargs=None): 1402 """ 1403 Execute anaddb to compute the interatomic forces. 1404 1405 Args: 1406 ifcout: Number of neighbouring atoms for which the ifc's will be output. If None all the atoms in the big box. 1407 asr, chneut, dipdip: Anaddb input variable. See official documentation. 1408 ngqpt: Number of divisions for the q-mesh in the DDB file. Auto-detected if None (default) 1409 mpi_procs: Number of MPI processes to use. 1410 workdir: Working directory. If None, a temporary directory is created. 1411 manager: |TaskManager| object. If None, the object is initialized from the configuration file 1412 verbose: verbosity level. Set it to a value > 0 to get more information 1413 anaddb_kwargs: additional kwargs for anaddb 1414 1415 Returns: 1416 :class:`InteratomicForceConstants` with the calculated ifc. 1417 """ 1418 if ngqpt is None: ngqpt = self.guessed_ngqpt 1419 1420 inp = AnaddbInput.ifc(self.structure, ngqpt=ngqpt, ifcout=ifcout, q1shft=(0, 0, 0), asr=asr, chneut=chneut, 1421 dipdip=dipdip, anaddb_kwargs=anaddb_kwargs) 1422 1423 task = self._run_anaddb_task(inp, mpi_procs, workdir, manager, verbose) 1424 1425 anaddbnc_path = task.outpath_from_ext("anaddb.nc") 1426 return InteratomicForceConstants.from_file(anaddbnc_path) 1427 1428 def anaget_phonopy_ifc(self, ngqpt=None, supercell_matrix=None, asr=0, chneut=0, dipdip=0, 1429 manager=None, workdir=None, mpi_procs=1, symmetrize_tensors=False, 1430 output_dir_path=None, prefix_outfiles="", symprec=1e-5, set_masses=False, 1431 verbose=0): 1432 """ 1433 Runs anaddb to get the interatomic force constants(IFC), born effective charges(BEC) and dielectric 1434 tensor obtained and converts them to the phonopy format. Optionally writes the 1435 standard phonopy files to a selected directory: FORCE_CONSTANTS, BORN (if BECs are available) 1436 POSCAR of the unit cell, POSCAR of the supercell. 1437 1438 Args: 1439 ngqpt: the ngqpt used to generate the anaddbnc. Will be used to determine the (diagonal) 1440 supercell matrix in phonopy. A smaller value can be used, but some information will 1441 be lost and inconsistencies in the convertion may occour. 1442 supercell_matrix: the supercell matrix used for phonopy. if None it will be set to 1443 a diagonal matrix with ngqpt on the diagonal. This should provide the best agreement between 1444 the anaddb and phonopy results. 1445 asr, chneut, dipdip: Anaddb input variable. See official documentation. 1446 manager: |TaskManager| object. If None, the object is initialized from the configuration file 1447 workdir: Working directory. If None, a temporary directory is created. 1448 mpi_procs: Number of MPI processes to use. 1449 symmetrize_tensors: if True the tensors will be symmetrized in the Phonopy object and 1450 in the output files. This will apply to IFC, BEC and dielectric tensor. 1451 output_dir_path: a path to a directory where the phonopy files will be created 1452 prefix_outfiles: a string that will be added as a prefix to the name of the written files 1453 symprec: distance tolerance in Cartesian coordinates to find crystal symmetry in phonopy. 1454 It might be that the value should be tuned so that it leads to the the same symmetries 1455 as in the abinit calculation. 1456 set_masses: if True the atomic masses used by abinit will be added to the PhonopyAtoms 1457 and will be present in the returned Phonopy object. This should improve compatibility 1458 among abinit and phonopy results if frequencies needs to be calculated. 1459 verbose: verbosity level. Set it to a value > 0 to get more information 1460 1461 Returns: 1462 An instance of a Phonopy object that contains the IFC, BEC and dieletric tensor data. 1463 """ 1464 1465 if ngqpt is None: ngqpt = self.guessed_ngqpt 1466 if supercell_matrix is None: 1467 supercell_matrix = np.eye(3) * ngqpt 1468 1469 inp = AnaddbInput.ifc(self.structure, ngqpt=ngqpt, ifcout=None, q1shft=(0, 0, 0), asr=asr, 1470 chneut=chneut, dipdip=dipdip) 1471 1472 task = self._run_anaddb_task(inp, mpi_procs, workdir, manager, verbose=verbose) 1473 1474 from abipy.dfpt.anaddbnc import AnaddbNcFile 1475 from abipy.dfpt.converters import abinit_to_phonopy 1476 anaddbnc_path = task.outpath_from_ext("anaddb.nc") 1477 anaddbnc = AnaddbNcFile(anaddbnc_path) 1478 phon = abinit_to_phonopy(anaddbnc=anaddbnc, supercell_matrix=supercell_matrix, 1479 symmetrize_tensors=symmetrize_tensors, output_dir_path=output_dir_path, 1480 prefix_outfiles=prefix_outfiles, symprec=symprec, set_masses=set_masses) 1481 1482 return phon 1483 1484 def anaget_interpolated_ddb(self, qpt_list, asr=2, chneut=1, dipdip=1, ngqpt=None, workdir=None, 1485 manager=None, mpi_procs=1, verbose=0, anaddb_kwargs=None): 1486 """ 1487 Runs anaddb to generate an interpolated DDB file on a list of qpt. 1488 1489 Args: 1490 qpt_list: list of fractional coordinates of qpoints where the ddb should be interpolated. 1491 asr, chneut, dipdip: Anaddb input variable. See official documentation. 1492 ngqpt: Number of divisions for the q-mesh in the DDB file. Auto-detected if None (default). 1493 workdir: Working directory. If None, a temporary directory is created. 1494 mpi_procs: Number of MPI processes to use. 1495 manager: |TaskManager| object. If None, the object is initialized from the configuration file 1496 verbose: verbosity level. Set it to a value > 0 to get more information. 1497 anaddb_kwargs: additional kwargs for anaddb. 1498 """ 1499 1500 if ngqpt is None: ngqpt = self.guessed_ngqpt 1501 1502 inp = AnaddbInput(self.structure, anaddb_kwargs=anaddb_kwargs) 1503 1504 q1shft = [[0, 0, 0]] 1505 inp.set_vars( 1506 ifcflag=1, 1507 ngqpt=np.array(ngqpt), 1508 q1shft=q1shft, 1509 nqshft=len(q1shft), 1510 asr=asr, 1511 chneut=chneut, 1512 dipdip=dipdip, 1513 prtddb=1 1514 ) 1515 inp['qph1l'] = [list(q) + [1] for q in qpt_list] 1516 inp['nph1l'] = len(qpt_list) 1517 1518 task = self._run_anaddb_task(inp, mpi_procs, workdir, manager, verbose=verbose) 1519 1520 new_ddb_path = os.path.join(task.workdir, "run.abo_DDB") 1521 if not os.path.exists(new_ddb_path): 1522 new_ddb_path = os.path.join(task.workdir, "run_DDB") 1523 1524 return self.__class__(new_ddb_path) 1525 1526 def anaget_dielectric_tensor_generator(self, asr=2, chneut=1, dipdip=1, workdir=None, mpi_procs=1, 1527 manager=None, verbose=0, anaddb_kwargs=None, return_input=False): 1528 """ 1529 Execute anaddb to extract the quantities necessary to create a |DielectricTensorGenerator|. 1530 Requires phonon perturbations at Gamma and static electric field perturbations. 1531 1532 Args: 1533 asr, chneut, dipdip: Anaddb input variable. See official documentation. 1534 workdir: Working directory. If None, a temporary directory is created. 1535 mpi_procs: Number of MPI processes to use. 1536 manager: |TaskManager| object. If None, the object is initialized from the configuration file 1537 verbose: verbosity level. Set it to a value > 0 to get more information 1538 anaddb_kwargs: additional kwargs for anaddb 1539 return_input: True if |AnaddbInput| object should be returned as 2nd argument 1540 1541 Return: |DielectricTensorGenerator| object. 1542 """ 1543 # Check if gamma is in the DDB. 1544 try: 1545 self.qindex((0, 0, 0)) 1546 except Exception: 1547 raise ValueError("Gamma point not in %s.\nddb.qpoints:\n%s" % (self.filepath, self.qpoints)) 1548 1549 inp = AnaddbInput.modes_at_qpoint(self.structure, (0, 0, 0), asr=asr, chneut=chneut, dipdip=dipdip, 1550 lo_to_splitting=False, anaddb_kwargs=anaddb_kwargs) 1551 1552 if anaddb_kwargs is None or 'dieflag' not in anaddb_kwargs: 1553 inp['dieflag'] = 1 1554 1555 task = self._run_anaddb_task(inp, mpi_procs, workdir, manager, verbose) 1556 1557 phbstnc_path = task.outpath_from_ext("PHBST.nc") 1558 anaddbnc_path = task.outpath_from_ext("anaddb.nc") 1559 1560 gen = DielectricTensorGenerator.from_files(phbstnc_path, anaddbnc_path) 1561 1562 return gen if not return_input else (gen, inp) 1563 1564 def anaget_elastic(self, relaxed_ion="automatic", piezo="automatic", 1565 dde=False, stress_correction=False, asr=2, chneut=1, 1566 mpi_procs=1, workdir=None, manager=None, verbose=0, retpath=False): 1567 """ 1568 Call anaddb to compute elastic and piezoelectric tensors. Require DDB with strain terms. 1569 1570 By default, this method sets the anaddb input variables automatically 1571 by looking at the 2nd-order derivatives available in the DDB file. 1572 This behaviour can be changed by setting explicitly the value of: 1573 `relaxed_ion` and `piezo`. 1574 1575 Args: 1576 relaxed_ion: Activate computation of relaxed-ion tensors. 1577 Allowed values are [True, False, "automatic"]. Defaults to "automatic". 1578 In "automatic" mode, relaxed-ion tensors are automatically computed if 1579 internal strain terms and phonons at Gamma are present in the DDB. 1580 piezo: Activate computation of piezoelectric tensors. 1581 Allowed values are [True, False, "automatic"]. Defaults to "automatic". 1582 In "automatic" mode, piezoelectric tensors are automatically computed if 1583 piezoelectric terms are present in the DDB. 1584 NB: relaxed-ion piezoelectric requires the activation of `relaxed_ion`. 1585 dde: if True, dielectric tensors will be calculated. 1586 stress_correction: Calculate the relaxed ion elastic tensors, considering 1587 the stress left inside cell. The DDB must contain the stress tensor. 1588 asr: Anaddb input variable. See official documentation. 1589 chneut: Anaddb input variable. See official documentation. 1590 mpi_procs: Number of MPI processes to use. 1591 workdir: Working directory. If None, a temporary directory is created. 1592 manager: |TaskManager| object. If None, the object is initialized from the configuration file 1593 verbose: verbosity level. Set it to a value > 0 to get more information 1594 retpath: True to return path to anaddb.nc file. 1595 1596 Return: 1597 |ElasticData| object if ``retpath`` is None else absolute path to anaddb.nc file. 1598 """ 1599 if not self.has_strain_terms(): # DOH! 1600 cprint("Strain perturbations are not available in DDB: %s" % self.filepath, "yellow") 1601 1602 if relaxed_ion == "automatic": 1603 relaxed_ion = self.has_internalstrain_terms() and self.has_at_least_one_atomic_perturbation(qpt=(0, 0, 0)) 1604 1605 if relaxed_ion: 1606 if not self.has_at_least_one_atomic_perturbation(qpt=(0, 0, 0)): 1607 cprint("Requiring `relaxed_ion` but no atomic term available in DDB: %s" % self.filepath, "yellow") 1608 if not self.has_internalstrain_terms(): 1609 cprint("Requiring `internal_strain` but no internal strain term in DDB: %s" % self.filepath, "yellow") 1610 1611 if piezo == "automatic": 1612 piezo = self.has_piezoelectric_terms() 1613 1614 if piezo and not self.has_piezoelectric_terms(): 1615 cprint("Requiring `piezo` but no piezoelectric term available in DDB: %s" % self.filepath, "yellow") 1616 1617 # FIXME This is problematic so don't use automatic as default 1618 #select = "all" 1619 select = "at_least_one_diagoterm" 1620 if dde == "automatic": 1621 dde = self.has_epsinf_terms(select=select) 1622 1623 if dde and not self.has_epsinf_terms(select=select): 1624 cprint("Requiring `dde` but dielectric tensor not available in DDB: %s" % self.filepath, "yellow") 1625 1626 if stress_correction == "automatic": 1627 stress_correction = self.cart_stress_tensor is not None 1628 1629 if stress_correction and self.cart_stress_tensor is None: 1630 cprint("Requiring `stress_correction` but stress not available in DDB: %s" % self.filepath, "yellow") 1631 1632 inp = AnaddbInput.dfpt(self.structure, strain=True, relaxed_ion=relaxed_ion, 1633 dde=dde, piezo=piezo, stress_correction=stress_correction, dte=False, 1634 asr=asr, chneut=chneut) 1635 1636 task = self._run_anaddb_task(inp, mpi_procs, workdir, manager, verbose) 1637 1638 # Read data from the netcdf output file produced by anaddb. 1639 anaddbnc_path = task.outpath_from_ext("anaddb.nc") 1640 return ElasticData.from_file(anaddbnc_path) if not retpath else anaddbnc_path 1641 1642 def anaget_raman(self, asr=2, chneut=1, ramansr=1, alphon=1, workdir=None, mpi_procs=1, 1643 manager=None, verbose=0, directions=None, anaddb_kwargs=None): 1644 """ 1645 Execute anaddb to compute the Raman spectrum 1646 1647 Args: 1648 qpoint: Reduced coordinates of the qpoint where phonon modes are computed. 1649 asr, chneut, ramansr, alphon: Anaddb input variable. See official documentation. 1650 workdir: Working directory. If None, a temporary directory is created. 1651 mpi_procs: Number of MPI processes to use. 1652 manager: |TaskManager| object. If None, the object is initialized from the configuration file 1653 verbose: verbosity level. Set it to a value > 0 to get more information. 1654 directions: list of 3D directions along which the non analytical contribution will be calculated. 1655 If None the three cartesian direction will be used. 1656 anaddb_kwargs: additional kwargs for anaddb. 1657 1658 Return: |Raman| object. 1659 """ 1660 inp = AnaddbInput.dfpt(self.structure, raman=True, asr=asr, chneut=chneut, ramansr=ramansr, 1661 alphon=alphon, directions=directions, anaddb_kwargs=anaddb_kwargs) 1662 1663 task = self._run_anaddb_task(inp, mpi_procs, workdir, manager, verbose) 1664 1665 # Read data from the netcdf output file produced by anaddb. 1666 anaddbnc_path = task.outpath_from_ext("anaddb.nc") 1667 return Raman.from_file(anaddbnc_path) 1668 1669 def _run_anaddb_task(self, anaddb_input, mpi_procs, workdir, manager, verbose): 1670 """ 1671 Execute an |AnaddbInput| via the shell. Return |AnaddbTask|. 1672 """ 1673 task = AnaddbTask.temp_shell_task(anaddb_input, ddb_node=self.filepath, 1674 mpi_procs=mpi_procs, workdir=workdir, manager=manager) 1675 1676 if verbose: 1677 print("ANADDB INPUT:\n", anaddb_input) 1678 print("workdir:", task.workdir) 1679 1680 # Run the task here. 1681 task.start_and_wait(autoparal=False) 1682 1683 report = task.get_event_report() 1684 if not report.run_completed: 1685 raise self.AnaddbError(task=task, report=report) 1686 1687 return task 1688 1689 def write(self, filepath, filter_blocks=None): 1690 """ 1691 Writes the DDB file in filepath. Requires the blocks data. 1692 Only the information stored in self.header.lines and in self.blocks will be used to produce the file 1693 """ 1694 lines = list(self.header.lines) 1695 1696 if filter_blocks is None: 1697 blocks = self.blocks 1698 else: 1699 blocks = [self.blocks[i] for i in filter_blocks] 1700 1701 lines.append(" **** Database of total energy derivatives ****") 1702 lines.append(" Number of data blocks={0:5}".format(len(blocks))) 1703 lines.append(" ") 1704 1705 for b in blocks: 1706 lines.extend(b["data"]) 1707 lines.append(" ") 1708 1709 lines.append(" List of bloks and their characteristics") 1710 lines.append(" ") 1711 1712 for b in blocks: 1713 lines.extend(b["data"][:2]) 1714 lines.append(" ") 1715 1716 with open(filepath, "wt") as f: 1717 f.write("\n".join(lines)) 1718 1719 def get_block_for_qpoint(self, qpt): 1720 """ 1721 Extracts the block data for the selected qpoint. 1722 Returns a list of lines containing the block information 1723 """ 1724 if hasattr(qpt, "frac_coords"): qpt = qpt.frac_coords 1725 1726 for b in self.blocks: 1727 if b['qpt'] is not None and np.allclose(b['qpt'], qpt): 1728 return b["data"] 1729 1730 def replace_block_for_qpoint(self, qpt, data): 1731 """ 1732 Change the block data for the selected qpoint. Object is modified in-place. 1733 Data should be a list of strings representing the whole data block. 1734 Note that the DDB file should be written and reopened in order to call methods that run anaddb. 1735 1736 Return: 1737 True if qpt has been found and data has been replaced. 1738 """ 1739 if hasattr(qpt, "frac_coords"): qpt = qpt.frac_coords 1740 1741 for b in self.blocks: 1742 if b['qpt'] is not None and np.allclose(b['qpt'], qpt): 1743 b["data"] = data 1744 return True 1745 1746 return False 1747 1748 def insert_block(self, data, replace=True): 1749 """ 1750 Inserts a block in the list. Can replace a block if already present. 1751 1752 Args: 1753 data: a dictionary with keys "dord" (the order of the perturbation), 1754 "qpt" (the fractional coordinates of the qpoint if dord=2), 1755 "qpt3" (the fractional coordinates of the qpoints if dord=3), 1756 "data" (the lines of the block of data). 1757 replace: if True and an equivalent block is already present it will be replaced, 1758 otherwise the block will not be inserted. 1759 1760 Returns: 1761 bool: True if the block was inserted. 1762 """ 1763 dord = data["dord"] 1764 for i, b in enumerate(self.blocks): 1765 if dord == b["dord"] and \ 1766 (dord in (0, 1) or 1767 (dord == 2 and np.allclose(b['qpt'], data["qpt"])) or 1768 (dord == 3 and np.allclose(b['qpt3'], data["qpt3"]))): 1769 if replace: 1770 self.blocks[i] = data 1771 return True 1772 else: 1773 return False 1774 1775 self.blocks.append(data) 1776 return True 1777 1778 def remove_block(self, dord, qpt=None, qpt3=None): 1779 """ 1780 Removes one block from the list of blocks in the ddb 1781 Args: 1782 dord: the order of the perturbation (from 0 to 3). 1783 qpt: the fractional coordinates of the q point of the block to be 1784 removed. Should be present if dord=2. 1785 qpt3: a 3x3 matrix with the coordinates for the third order perturbations. 1786 Should be present in dord=3. 1787 1788 Returns: 1789 bool: True if a matching block was found and removed. 1790 """ 1791 if dord == 2 and qpt is None: 1792 raise ValueError("if dord==2 the qpt should be set") 1793 if dord == 3 and qpt3 is None: 1794 raise ValueError("if dord==3 the qpt3 should be set") 1795 1796 for i, b in enumerate(self.blocks): 1797 if dord == b["dord"] and \ 1798 (dord in (0, 1) or 1799 (dord == 2 and np.allclose(b['qpt'], qpt)) or 1800 (dord == 3 and np.allclose(b['qpt3'], qpt3))): 1801 self.blocks.pop(i) 1802 return True 1803 1804 return False 1805 1806 def get_2nd_ord_dict(self): 1807 """ 1808 Generates an ordered dictionary with the second order derivative of the form 1809 {qpt: {(idir1, ipert1, idir2, ipert2): complex value}}. 1810 1811 Returns: 1812 OrderedDict: a dictionary with all the elements of a dynamical matrix 1813 """ 1814 dynmat = self.computed_dynmat 1815 d = OrderedDict() 1816 1817 for q, dm in dynmat.items(): 1818 dd = {} 1819 for index, row in dm.iterrows(): 1820 dd[index] = row["cvalue"] 1821 d[q] = dd 1822 1823 return d 1824 1825 def set_2nd_ord_data(self, data, replace=True): 1826 """ 1827 Insert the blocks corresponding to the data provided for the second 1828 order perturbations. 1829 1830 Args: 1831 data: a dict of the form {qpt: {(idir1, ipert1, idir2, ipert2): complex value}}. 1832 replace: if True and an equivalent block is already present it will be replaced, 1833 otherwise the block will not be inserted. 1834 """ 1835 for q, d in data.items(): 1836 if isinstance(q, Kpoint): 1837 q = q.frac_coords 1838 lines = get_2nd_ord_block_string(q, d) 1839 block_data = {"qpt": q, "qpt3": None, "dord": 2, "data": lines} 1840 1841 self.insert_block(block_data, replace=replace) 1842 1843 def get_panel(self): 1844 """Build panel with widgets to interact with the |DdbFile| either in a notebook or in panel app.""" 1845 from abipy.panels.ddb import DdbFilePanel 1846 return DdbFilePanel(self).get_panel() 1847 1848 def write_notebook(self, nbpath=None): 1849 """ 1850 Write an jupyter_ notebook to nbpath. If ``nbpath`` is None, a temporay file in the current 1851 working directory is created. Return path to the notebook. 1852 """ 1853 nbformat, nbv, nb = self.get_nbformat_nbv_nb(title=None) 1854 1855 nb.cells.extend([ 1856 nbv.new_code_cell("ddb = abilab.abiopen('%s')" % self.filepath), 1857 nbv.new_code_cell("units = 'eV'\nprint(ddb)"), 1858 nbv.new_code_cell("# display(ddb.header)"), 1859 nbv.new_markdown_cell("## Invoke `anaddb` to compute bands and dos"), 1860 nbv.new_code_cell("""\ 1861bstfile, phdosfile = ddb.anaget_phbst_and_phdos_files(nqsmall=10, ndivsm=20, 1862 asr=2, chneut=1, dipdip=0, lo_to_splitting="automatic", 1863 dos_method="tetra", ngqpt=None, qptbounds=None, verbose=0, anaddb_kwargs=None) 1864 1865phbands, phdos = bstfile.phbands, phdosfile.phdos"""), 1866 nbv.new_markdown_cell("## q-point path"), 1867 nbv.new_code_cell("phbands.qpoints.plot();"), 1868 nbv.new_markdown_cell("## Phonon bands with DOS"), 1869 nbv.new_code_cell("phbands.plot_with_phdos(phdos, units=units);"), 1870 nbv.new_markdown_cell("## Phonon fatbands with DOS"), 1871 nbv.new_code_cell("phbands.plot_fatbands(phdos_file=phdosfile, units=units);"), 1872 nbv.new_markdown_cell("## Distribution of phonon frequencies wrt mode index"), 1873 nbv.new_code_cell("phbands.boxplot(units=units);"), 1874 nbv.new_markdown_cell("## Phonon band structure with different color for each line"), 1875 nbv.new_code_cell("phbands.plot_colored_matched(units=units);"), 1876 nbv.new_markdown_cell("## Type-projected phonon DOS."), 1877 nbv.new_code_cell("phdosfile.plot_pjdos_type(units=units);"), 1878 nbv.new_markdown_cell("## Type-projected phonon DOS decomposed along the three reduced directions"), 1879 nbv.new_code_cell("phdosfile.plot_pjdos_redirs_type(units=units);"), 1880 nbv.new_code_cell("#phdosfile.plot_pjdos_redirs_site(units=units);"), 1881 nbv.new_markdown_cell("## Thermodinamic properties within the harmonic approximation"), 1882 nbv.new_code_cell("phdosfile.phdos.plot_harmonic_thermo(tstart=5, tstop=300);"), 1883 1884 nbv.new_markdown_cell("## Macroscopic dielectric tensor and Born effective charges"), 1885 nbv.new_code_cell("""\ 1886if False: 1887 eps_inf, becs = ddb.anaget_epsinf_and_becs() 1888 print(eps_inf) 1889 print(becs)"""), 1890 1891 nbv.new_markdown_cell("## Call `anaddb` to compute phonons and DOS with/without ASR"), 1892 nbv.new_code_cell("""\ 1893#asr_plotter = ddb.anacompare_asr(asr_list=(0, 2), nqsmall=0, ndivsm=10) 1894#asr_plotter.gridplot(); 1895"""), 1896 1897 nbv.new_markdown_cell("## Call `anaddb` to compute phonon DOS with different BZ samplings"), 1898 nbv.new_code_cell("""\ 1899c = None 1900if False: 1901 c = ddb.anacompare_phdos(nqsmalls=[20, 30], asr=2, chneut=1, dipdip=1, 1902 dos_method="tetra", ngqpt=None, num_cpus=1)"""), 1903 1904 nbv.new_code_cell("""\ 1905if c is not None: 1906 c.plotter.ipw_select_plot() 1907 #for phdos in c.phdoses"""), 1908 1909 nbv.new_markdown_cell("## Analysis of the IFCs in real-space"), 1910 nbv.new_code_cell("""\ 1911ifc = None 1912if False: 1913 ifc = ddb.anaget_ifc(ifcout=None, asr=2, chneut=1, dipdip=1, ngqpt=None, verbose=0, anaddb_kwargs=None)"""), 1914 1915 nbv.new_code_cell("""\ 1916if ifc is not None: 1917 ifc.plot_longitudinal_ifc(atom_indices=None, atom_element=None, neighbour_element=None, min_dist=None, 1918 max_dist=None, ax=None);"""), 1919 nbv.new_code_cell("""\ 1920if ifc is not None: 1921 ifc.plot_longitudinal_ifc_short_range(atom_indices=None, atom_element=None, neighbour_element=None);"""), 1922 nbv.new_code_cell("""\ 1923if ifc is not None: 1924 ifc.plot_longitudinal_ifc_ewald(atom_indices=None, atom_element=None, neighbour_element=None, 1925 min_dist=None, max_dist=None, ax=None);"""), 1926 ]) 1927 1928 return self._write_nb_nbpath(nb, nbpath) 1929 1930 1931class Becs(Has_Structure, MSONable): 1932 """ 1933 This object stores the Born effective charges and provides simple tools for data analysis. 1934 """ 1935 1936 @pmg_serialize 1937 def as_dict(self): 1938 """Return dictionary with JSON serialization in MSONable format.""" 1939 return dict(becs_arr=self.values, structure=self.structure, chneut=self.chneut, order="c") 1940 1941 def __init__(self, becs_arr, structure, chneut, order="c"): 1942 """ 1943 Args: 1944 becs_arr: [3, 3, natom] array with the Born effective charges in Cartesian coordinates. 1945 structure: |Structure| object. 1946 chneut: Option used for the treatment of the Charge Neutrality. 1947 for the effective charges (anaddb input variable) 1948 order: "f" if becs_arr is in Fortran order. 1949 """ 1950 assert len(becs_arr) == len(structure) 1951 self._structure = structure 1952 self.chneut = chneut 1953 1954 # Values is a numpy array while zstars is a list of Tensor objects. 1955 self.values = np.empty((len(structure), 3, 3)) 1956 for i, bec in enumerate(becs_arr): 1957 mat = becs_arr[i] 1958 if order.lower() == "f": mat = mat.T.copy() 1959 self.values[i] = mat 1960 1961 self.zstars = [ZstarTensor(mat) for mat in self.values] 1962 1963 @property 1964 def structure(self): 1965 """|Structure| object.""" 1966 return self._structure 1967 1968 def __repr__(self): 1969 return self.to_string() 1970 1971 def to_string(self, verbose=0): 1972 """String representation.""" 1973 lines = []; app = lines.append 1974 app("Born effective charges in Cartesian coordinates (Voigt notation)") 1975 app(self.get_voigt_dataframe().to_string()) 1976 app("") 1977 1978 if verbose: 1979 app("Born effective charges (full tensor)") 1980 for site, bec in zip(self.structure, self.values): 1981 app("Z* at site: %s" % repr(site)) 1982 app(str(bec)) 1983 app("") 1984 1985 # Add info on the bec sum rule. 1986 app("Born effective charge neutrality sum-rule with chneut: %d\n" % self.chneut) 1987 app(str(self.sumrule)) 1988 1989 return "\n".join(lines) 1990 1991 @property 1992 def sumrule(self): 1993 """[3, 3] matrix with Born effective charge neutrality sum-rule.""" 1994 return self.values.sum(axis=0) 1995 1996 def _repr_html_(self): 1997 """Integration with jupyter notebooks.""" 1998 return self.get_voigt_dataframe()._repr_html_() 1999 2000 def get_voigt_dataframe(self, view="inequivalent", tol=1e-3, select_symbols=None, decimals=5, verbose=0): 2001 """ 2002 Return |pandas-DataFrame| with Voigt indices as columns and natom rows. 2003 2004 Args: 2005 view: "inequivalent" to show only inequivalent atoms. "all" for all sites. 2006 tol: Entries are set to zero below this value 2007 select_symbols: String or list of strings with chemical symbols. 2008 Used to select only atoms of this type. 2009 decimals: Number of decimal places to round to. 2010 If decimals is negative, it specifies the number of positions to the left of the decimal point. 2011 verbose: Verbosity level. 2012 """ 2013 aview = self._get_atomview(view, select_symbols=select_symbols, verbose=verbose) 2014 2015 columns = ["xx", "yy", "zz", "yz", "xz", "xy"] 2016 rows = [] 2017 for (iatom, wlabel) in zip(aview.iatom_list, aview.wyck_labels): 2018 site = self.structure[iatom] 2019 zstar = self.zstars[iatom] 2020 d = OrderedDict() 2021 d["element"] = site.specie.symbol 2022 d["site_index"] = iatom 2023 d["frac_coords"] = np.round(site.frac_coords, decimals=decimals) 2024 d["cart_coords"] = np.round(site.coords, decimals=decimals) 2025 d["wyckoff"] = wlabel 2026 zstar = zstar.zeroed(tol=tol) 2027 for k, v in zip(columns, zstar.voigt): 2028 d[k] = v 2029 if verbose: 2030 d["determinant"] = np.linalg.det(zstar) 2031 d["iso"] = zstar.trace() / 3 2032 rows.append(d) 2033 2034 return pd.DataFrame(rows, columns=list(rows[0].keys()) if rows else None) 2035 2036 def check_site_symmetries(self, verbose=0): 2037 """ 2038 Check site symmetries of the Born effective charges. Print output to terminal. 2039 Return: max_err 2040 """ 2041 return self.structure.site_symmetries.check_site_symmetries(self.values, verbose=verbose) 2042 2043 2044class DielectricTensorGenerator(Has_Structure): 2045 """ 2046 Object used to generate frequency dependent dielectric tensors as obtained 2047 from DFPT calculations. The values are calculated on the fly 2048 based on the phonon frequencies at gamma and oscillator strengths. 2049 The first three frequencies would be considered as acoustic modes and 2050 ignored in the calculation. No checks would be performed. 2051 2052 See the definitions Eq.(53-54) in :cite:`Gonze1997` PRB55, 10355 (1997). 2053 """ 2054 2055 @classmethod 2056 def from_files(cls, phbst_filepath, anaddbnc_filepath): 2057 """ 2058 Generates the object from the files that contain the phonon frequencies, oscillator strength and 2059 static dielectric tensor, i.e. the PHBST.nc and anaddb.nc netcdf files, respectively. 2060 """ 2061 with ETSF_Reader(phbst_filepath) as reader: 2062 qpts = reader.read_value("qpoints") 2063 full_phfreqs = reader.read_value("phfreqs") 2064 2065 for i, q in enumerate(qpts): 2066 if np.array_equal(q, [0, 0, 0]): 2067 phfreqs = full_phfreqs[i].copy() 2068 break 2069 else: 2070 raise ValueError('The PHBST does not contain frequencies at gamma') 2071 2072 with ETSF_Reader(anaddbnc_filepath) as reader: 2073 epsinf = DielectricTensor(reader.read_value("emacro_cart").T.copy()) 2074 eps0 = DielectricTensor(reader.read_value("emacro_cart_rlx").T.copy()) 2075 try: 2076 oscillator_strength = reader.read_value("oscillator_strength", cmode="c") 2077 oscillator_strength = oscillator_strength.transpose((0, 2, 1)).copy() 2078 except Exception as exc: 2079 import traceback 2080 msg = traceback.format_exc() 2081 msg += ("Error while trying to read from file.\n" 2082 "Verify that dieflag == 1, 3 or 4 in anaddb\n") 2083 raise ValueError(msg) 2084 2085 structure = reader.read_structure() 2086 2087 return cls(phfreqs, oscillator_strength, eps0, epsinf, structure) 2088 2089 @classmethod 2090 def from_objects(cls, phbands, anaddbnc): 2091 """ 2092 Generates the object from the objects |PhononBands| object and AnaddbNcFile 2093 """ 2094 gamma_index = phbands.qindex([0, 0, 0]) 2095 2096 phfreqs = phbands.phfreqs[gamma_index] 2097 2098 epsinf = anaddbnc.epsinf 2099 eps0 = anaddbnc.eps0 2100 oscillator_strength = anaddbnc.oscillator_strength 2101 if epsinf is None or eps0 is None or oscillator_strength is None: 2102 raise ValueError("Could not instantiate from the provided objects. " 2103 "Some information is missing.") 2104 2105 return cls(phfreqs, oscillator_strength, eps0, epsinf, anaddbnc.structure) 2106 2107 def __init__(self, phfreqs, oscillator_strength, eps0, epsinf, structure): 2108 """ 2109 Args: 2110 phfreqs: numpy array containing the 3 * num_atoms phonon frequencies at gamma 2111 oscillator_strength: complex numpy array with shape [number of phonon modes, 3, 3] in atomic units 2112 eps0: numpy array containing the e0 dielectric tensor without frequency dependence 2113 epsinf: numpy array with the electronic dielectric tensor (einf) without frequency dependence 2114 structure: |Structure| object. 2115 """ 2116 self.phfreqs = phfreqs 2117 self.oscillator_strength = oscillator_strength 2118 self.eps0 = eps0 2119 self.epsinf = epsinf 2120 self._structure = structure 2121 2122 @property 2123 def structure(self): 2124 """|Structure| object.""" 2125 return self._structure 2126 2127 def __str__(self): 2128 return self.to_string() 2129 2130 def to_string(self, verbose=0): 2131 """String representation with verbosity level `verbose`.""" 2132 lines = [] 2133 app = lines.append 2134 app(self.structure.to_string(verbose=verbose, title="Structure")) 2135 app("") 2136 app(marquee("Oscillator strength", mark="=")) 2137 tol = 1e-6 2138 app("Real part in Cartesian coordinates. a.u. units; 1 a.u. = 253.2638413 m3/s2. Set to zero below %.2e." % tol) 2139 app(self.get_oscillator_dataframe(reim="re", tol=tol).to_string()) 2140 if verbose: 2141 app("") 2142 app("Imaginary part in a.u.; 1 a.u. = 253.2638413 m3/s2. Set to zero below %.2e." % tol) 2143 app(self.get_oscillator_dataframe(reim="im", tol=tol).to_string()) 2144 app("") 2145 app("Trace of oscillator strength, for each phonon mode:") 2146 traces = [o.trace() for o in self.oscillator_strength] 2147 app(str(traces)) 2148 app("") 2149 2150 tol = 1e-3 2151 app(marquee("Dielectric Tensors", mark="=")) 2152 app("Electronic dielectric tensor (eps_inf) in Cartesian coordinates. Set to zero below %.2e." % tol) 2153 app(self.epsinf.get_dataframe(tol=tol).to_string()) 2154 app("") 2155 app("Zero-frequency dielectric tensor (eps_zero) in Cartesian coordinates. Set to zero below %.2e." % tol) 2156 app(self.eps0.get_dataframe(tol=tol).to_string()) 2157 2158 return "\n".join(lines) 2159 2160 def get_oscillator_dataframe(self, reim="all", tol=1e-6): 2161 """ 2162 Return |pandas-Dataframe| with oscillator matrix elements. 2163 2164 Args: 2165 reim: "re" for real part, "im" for imaginary part, "all" for both. 2166 tol: Entries are set to zero below this value 2167 """ 2168 dmap = dict(xx=(0, 0), yy=(1, 1), zz=(2, 2), yz=(1, 2), xz=(0, 2), xy=(0, 1)) 2169 #decimals = int(abs(np.rint(np.log10(tol)))) 2170 # 1 a.u. = 253.2638413 m3/s2. 2171 # TODO: Use SI? 2172 #fact = 253.2638413 2173 2174 rows, index = [], [] 2175 for nu in range(3 * len(self.structure)): 2176 d = {k: data_from_cplx_mode(reim, self.oscillator_strength[nu][t], tol=tol) for k, t in dmap.items()} 2177 #d = {k: np.around(v * fact, decimals=decimals) for k, v in d.items()} 2178 rows.append(d) 2179 index.append(nu) 2180 2181 df = pd.DataFrame(rows, index=index, columns=list(rows[0].keys())) 2182 df.index.name = "mode" 2183 return df 2184 2185 def tensor_at_frequency(self, w, gamma_ev=1e-4, units='eV'): 2186 """ 2187 Returns a |DielectricTensor| object representing the dielectric tensor 2188 in atomic units at the specified frequency w. Eq.(53-54) in PRB55, 10355 (1997). 2189 2190 Args: 2191 w: Frequency. 2192 gamma_ev: Phonon damping factor in eV (full width). Poles are shifted by phfreq * gamma_ev. 2193 Accept scalar or [nfreq] array. 2194 units: string specifying the units used for phonon frequencies. Possible values in 2195 ("eV", "meV", "Ha", "cm-1", "Thz"). Case-insensitive. 2196 """ 2197 w = w / phfactor_ev2units(units) 2198 2199 # Note that the acoustic modes are not included: their oscillator strength should be exactly zero 2200 # Also, only the real part of the oscillators is taken into account: 2201 # the possible imaginary parts of degenerate modes will cancel. 2202 if duck.is_listlike(gamma_ev): 2203 gammas = np.asarray(gamma_ev) 2204 assert len(gammas) == len(self.phfreqs) 2205 else: 2206 gammas = np.ones(len(self.phfreqs)) * float(gamma_ev) 2207 2208 t = np.zeros((3, 3),dtype=complex) 2209 for i in range(3, len(self.phfreqs)): 2210 g = gammas[i] * self.phfreqs[i] 2211 t += self.oscillator_strength[i].real / (self.phfreqs[i]**2 - w**2 - 1j*g) 2212 2213 vol = self.structure.volume / bohr_to_angstrom ** 3 2214 t = 4 * np.pi * t / vol / eV_to_Ha ** 2 2215 t += self.epsinf 2216 2217 return DielectricTensor(t) 2218 2219 @add_fig_kwargs 2220 def plot(self, w_min=0, w_max=None, gamma_ev=1e-4, num=500, component='diag', reim="reim", units='eV', 2221 with_phfreqs=True, ax=None, fontsize=12, **kwargs): 2222 """ 2223 Plots the selected components of the dielectric tensor as a function of frequency. 2224 2225 Args: 2226 w_min: minimum frequency in units `units`. 2227 w_max: maximum frequency. If None it will be set to the value of the maximum frequency + 5*gamma_ev. 2228 gamma_ev: Phonon damping factor in eV (full width). Poles are shifted by phfreq * gamma_ev. 2229 Accept scalar or [nfreq] array. 2230 num: number of values of the frequencies between w_min and w_max. 2231 component: determine which components of the tensor will be displayed. Can be a list/tuple of two 2232 elements, indicating the indices [i, j] of the desired component or a string among: 2233 2234 * 'diag_av' to plot the average of the components on the diagonal 2235 * 'diag' to plot the elements on diagonal 2236 * 'all' to plot all the components in the upper triangle. 2237 * 'offdiag' to plot the off-diagonal components in the upper triangle. 2238 2239 reim: a string with "re" will plot the real part, with "im" selects the imaginary part. 2240 units: string specifying the units used for phonon frequencies. Possible values in 2241 ("eV", "meV", "Ha", "cm-1", "Thz"). Case-insensitive. 2242 with_phfreqs: True to show phonon frequencies with dots. 2243 ax: |matplotlib-Axes| or None if a new figure should be created. 2244 fontsize: Legend and label fontsize. 2245 2246 Return: |matplotlib-Figure| 2247 """ 2248 wmesh = self._get_wmesh(gamma_ev, num, units, w_min, w_max) 2249 t = np.zeros((num, 3, 3), dtype=complex) 2250 2251 for i, w in enumerate(wmesh): 2252 t[i] = self.tensor_at_frequency(w, units=units, gamma_ev=gamma_ev) 2253 2254 ax, fig, plt = get_ax_fig_plt(ax=ax) 2255 2256 if 'linewidth' not in kwargs: 2257 kwargs['linewidth'] = 2 2258 2259 ax.set_xlabel('Frequency {}'.format(phunit_tag(units))) 2260 ax.set_ylabel(r'$\epsilon(\omega)$') 2261 ax.grid(True) 2262 2263 reimfs = [] 2264 if 're' in reim: reimfs.append((np.real, "Re{%s}")) 2265 if 'im' in reim: reimfs.append((np.imag, "Im{%s}")) 2266 2267 for reimf, reims in reimfs: 2268 if isinstance(component, (list, tuple)): 2269 label = reims % r'$\epsilon_{%d%d}$' % tuple(component) 2270 ax.plot(wmesh, reimf(t[:,component[0], component[1]]), label=label, **kwargs) 2271 elif component == 'diag': 2272 for i in range(3): 2273 label = reims % r'$\epsilon_{%d%d}$' % (i, i) 2274 ax.plot(wmesh, reimf(t[:, i, i]), label=label, **kwargs) 2275 elif component in ('all', "offdiag"): 2276 for i in range(3): 2277 for j in range(3): 2278 if component == "all" and i > j: continue 2279 if component == "offdiag" and i >= j: continue 2280 label = reims % r'$\epsilon_{%d%d}$' % (i, j) 2281 ax.plot(wmesh, reimf(t[:, i, j]), label=label, **kwargs) 2282 elif component == 'diag_av': 2283 label = r'$Average\, %s\epsilon_{ii}$' % reims 2284 ax.plot(wmesh, np.trace(reimf(t), axis1=1, axis2=2)/3, label=label, **kwargs) 2285 else: 2286 raise ValueError('Unkwnown component {}'.format(component)) 2287 2288 self._add_phfreqs(ax, units, with_phfreqs) 2289 2290 ax.legend(loc="best", fontsize=fontsize, shadow=True) 2291 2292 return fig 2293 2294 def _get_wmesh(self, gamma_ev, num, units, w_min, w_max): 2295 """ 2296 Helper function to get the wmesh for the plots. 2297 2298 Args: 2299 gamma_ev: Phonon damping factor in eV (full width). Poles are shifted by phfreq * gamma_ev. 2300 Accept scalar or [nfreq] array. 2301 num: number of values of the frequencies between w_min and w_max. 2302 units: string specifying the units used for phonon frequencies. Possible values in 2303 ("eV", "meV", "Ha", "cm-1", "Thz"). Case-insensitive. 2304 w_min: minimum frequency in units `units`. 2305 w_max: maximum frequency. If None it will be set to the value of the maximum frequency + 5*gamma_ev. 2306 2307 Returns: 2308 a numpy array with the frequencies. 2309 """ 2310 if w_max is None: 2311 w_max = (np.max(self.phfreqs) + gamma_ev * 10) * phfactor_ev2units(units) 2312 2313 wmesh = np.linspace(w_min, w_max, num, endpoint=True) 2314 return wmesh 2315 2316 # To maintain backward compatibility. 2317 plot_vs_w = plot 2318 2319 @add_fig_kwargs 2320 def plot_all(self, **kwargs): 2321 """ 2322 Plot diagonal and off-diagonal elements of the dielectric tensor as a function of frequency. 2323 Both real and imag part are show. Accepts all arguments of `plot` method with the exception of: 2324 `component` and `reim`. 2325 2326 Returns: |matplotlib-Figure| 2327 """ 2328 axmat, fig, plt = get_axarray_fig_plt(None, nrows=2, ncols=2, 2329 sharex=True, sharey=False, squeeze=False) 2330 fontsize = kwargs.pop("fontsize", 8) 2331 for irow in range(2): 2332 component = {0: "diag", 1: "offdiag"}[irow] 2333 for icol in range(2): 2334 reim = {0: "re", 1: "im"}[icol] 2335 self.plot(component=component, reim=reim, ax=axmat[irow, icol], fontsize=fontsize, show=False, **kwargs) 2336 2337 return fig 2338 2339 @add_fig_kwargs 2340 def plot_e0w_qdirs(self, qdirs=None, w_min=0, w_max=None, gamma_ev=1e-4, num=500, reim="reim", func="direct", 2341 units='eV', with_phfreqs=True, ax=None, fontsize=12, **kwargs): 2342 r""" 2343 Plots the dielectric tensor and/or -epsinf_q**2 / \epsilon_q along a set of specified directions. 2344 With \epsilon_q as defined in eq. (56) in :cite:`Gonze1997` PRB55, 10355 (1997). 2345 2346 Args: 2347 qdirs: a list of directions along which to plot the dielectric tensor. They will be normalized 2348 internally. If None the three cartesian directions will be shown. 2349 w_min: minimum frequency in units `units`. 2350 w_max: maximum frequency. If None it will be set to the value of the maximum frequency + 5*gamma_ev. 2351 gamma_ev: Phonon damping factor in eV (full width). Poles are shifted by phfreq * gamma_ev. 2352 Accept scalar or [nfreq] array. 2353 num: number of values of the frequencies between w_min and w_max. 2354 reim: a string with "re" will plot the real part, with "im" selects the imaginary part. 2355 func: determines which functional form will be plot. Can be: 2356 * "direct": plot of \epsilon_q 2357 * "inverse": plot of -epsinf_q**2 / \epsilon_q 2358 * "both": both plots. 2359 units: string specifying the units used for phonon frequencies. Possible values in 2360 ("eV", "meV", "Ha", "cm-1", "Thz"). Case-insensitive. 2361 with_phfreqs: True to show phonon frequencies with dots. 2362 ax: |matplotlib-Axes| or None if a new figure should be created. 2363 fontsize: Legend and label fontsize. 2364 2365 Return: |matplotlib-Figure| 2366 """ 2367 wmesh = self._get_wmesh(gamma_ev, num, units, w_min, w_max) 2368 t = np.zeros((num, 3, 3), dtype=complex) 2369 2370 for i, w in enumerate(wmesh): 2371 t[i] = self.tensor_at_frequency(w, units=units, gamma_ev=gamma_ev) 2372 2373 ax, fig, plt = get_ax_fig_plt(ax=ax) 2374 2375 if 'linewidth' not in kwargs: 2376 kwargs['linewidth'] = 2 2377 2378 ax.set_xlabel('Frequency {}'.format(phunit_tag(units))) 2379 ax.set_ylabel(r'$\epsilon(\omega)$') 2380 ax.grid(True) 2381 2382 reimfs = [] 2383 if 're' in reim: reimfs.append((np.real, "Re{%s}")) 2384 if 'im' in reim: reimfs.append((np.imag, "Im{%s}")) 2385 2386 if func == "both": 2387 func = ["direct", "inverse"] 2388 elif func not in ("direct", "inverse"): 2389 raise ValueError(f"unknown value for func: {func}") 2390 else: 2391 func = [func] 2392 2393 func_label = {"direct": r"$\epsilon_{{q{ind}}}$", 2394 "inverse": r"$-(\epsilon^{{\infty}}_{{q{ind}}})^2 / \epsilon_{{q{ind}}}$"} 2395 2396 if qdirs is None: 2397 qdirs = np.eye(3) 2398 elif len(np.shape(qdirs)) < 2: 2399 qdirs = [qdirs] 2400 2401 qdirs = np.array(qdirs, dtype=float) 2402 2403 for reimf, reims in reimfs: 2404 for func_form in func: 2405 for i, q in enumerate(qdirs): 2406 q /= np.linalg.norm(q) 2407 tt = np.einsum("i,kij,j->k", q, t, q) 2408 if func_form == "inverse": 2409 tt = -1 / tt * (np.einsum("i,ij,j", q, self.epsinf, q)) ** 2 2410 2411 label = reims % func_label[func_form].format(ind=i) 2412 ax.plot(wmesh, reimf(tt), label=label, **kwargs) 2413 2414 self._add_phfreqs(ax, units, with_phfreqs) 2415 2416 ax.legend(loc="best", fontsize=fontsize, shadow=True) 2417 2418 return fig 2419 2420 def _add_phfreqs(self, ax, units, with_phfreqs): 2421 """ 2422 Helper functions to add the phonon frequencies to the x axis. 2423 Args: 2424 ax: |matplotlib-Axes| or None if a new figure should be created. 2425 units: string specifying the units used for phonon frequencies. Possible values in 2426 ("eV", "meV", "Ha", "cm-1", "Thz"). Case-insensitive. 2427 with_phfreqs: True to show phonon frequencies with dots. 2428 """ 2429 # Add points showing phonon energies. 2430 if with_phfreqs: 2431 wvals = self.phfreqs[3:] * phfactor_ev2units(units) 2432 ax.scatter(wvals, np.zeros_like(wvals), s=30, marker="o", c="blue") 2433 2434 def reflectivity(self, qdir, w, gamma_ev=1e-4, units='eV'): 2435 """ 2436 Calculates the reflectivity from the dielectric tensor along the specified direction 2437 according to eq. (58) in :cite:`Gonze1997` PRB55, 10355 (1997). 2438 2439 Args: 2440 qdir: a list with three components defining the direction. 2441 It will be normalized internally. 2442 w: frequency. 2443 gamma_ev: Phonon damping factor in eV (full width). Poles are shifted by phfreq * gamma_ev. 2444 Accept scalar or [nfreq] array. 2445 units: string specifying the units used for phonon frequencies. Possible values in 2446 ("eV", "meV", "Ha", "cm-1", "Thz"). Case-insensitive. 2447 2448 Returns: 2449 the value of the reflectivity. 2450 """ 2451 2452 qdir = np.array(qdir) / np.linalg.norm(qdir) 2453 t = self.tensor_at_frequency(w, units=units, gamma_ev=gamma_ev) 2454 2455 n = np.einsum("i,ij,j", qdir, t, qdir) ** 0.5 2456 2457 r = (n - 1) / (n + 1) 2458 2459 return (r * r.conjugate()).real 2460 2461 @add_fig_kwargs 2462 def plot_reflectivity(self, qdirs=None, w_min=0, w_max=None, gamma_ev=1e-4, num=500, 2463 units='eV', with_phfreqs=True, ax=None, fontsize=12, **kwargs): 2464 """ 2465 Plots the reflectivity from the dielectric tensor along the specified directions, 2466 according to eq. (58) in :cite:`Gonze1997` PRB55, 10355 (1997). 2467 2468 Args: 2469 qdirs: a list of directions along which to plot the dielectric tensor. They will be normalized 2470 internally. If None the three cartesian directions will be shown. 2471 w_min: minimum frequency in units `units`. 2472 w_max: maximum frequency. If None it will be set to the value of the maximum frequency + 5*gamma_ev. 2473 gamma_ev: Phonon damping factor in eV (full width). Poles are shifted by phfreq * gamma_ev. 2474 Accept scalar or [nfreq] array. 2475 num: number of values of the frequencies between w_min and w_max. 2476 units: string specifying the units used for phonon frequencies. Possible values in 2477 ("eV", "meV", "Ha", "cm-1", "Thz"). Case-insensitive. 2478 with_phfreqs: True to show phonon frequencies with dots. 2479 ax: |matplotlib-Axes| or None if a new figure should be created. 2480 fontsize: Legend and label fontsize. 2481 2482 Return: |matplotlib-Figure| 2483 """ 2484 2485 wmesh = self._get_wmesh(gamma_ev, num, units, w_min, w_max) 2486 t = np.zeros((num, 3, 3), dtype=complex) 2487 2488 for i, w in enumerate(wmesh): 2489 t[i] = self.tensor_at_frequency(w, units=units, gamma_ev=gamma_ev) 2490 2491 if qdirs is None: 2492 qdirs = np.eye(3) 2493 elif len(np.shape(qdirs)) < 2: 2494 qdirs = [qdirs] 2495 2496 qdirs = np.array(qdirs, dtype=float) 2497 2498 qdirs /= np.linalg.norm(qdirs, axis=1)[:, None] 2499 2500 n = np.einsum("li,kij,lj->lk", qdirs, t, qdirs) ** 0.5 2501 2502 r = np.abs((n - 1) / (n + 1)) ** 2 2503 2504 ax, fig, plt = get_ax_fig_plt(ax=ax) 2505 2506 if 'linewidth' not in kwargs: 2507 kwargs['linewidth'] = 2 2508 2509 ax.set_xlabel('Frequency {}'.format(phunit_tag(units))) 2510 ax.set_ylabel(r'$R(\omega)$') 2511 ax.grid(True) 2512 2513 for i in range(len(qdirs)): 2514 label = f"$R_{{q{i}}}$" 2515 ax.plot(wmesh, r[i], label=label, **kwargs) 2516 2517 self._add_phfreqs(ax, units, with_phfreqs) 2518 2519 ax.legend(loc="best", fontsize=fontsize, shadow=True) 2520 2521 return fig 2522 2523 2524class DdbRobot(Robot): 2525 """ 2526 This robot analyzes the results contained in multiple DDB_ files. 2527 2528 .. rubric:: Inheritance Diagram 2529 .. inheritance-diagram:: DdbRobot 2530 """ 2531 EXT = "DDB" 2532 2533 @classmethod 2534 def class_handles_filename(cls, filename): 2535 """Exclude DDB.nc files. Override base class.""" 2536 return filename.endswith("_" + cls.EXT) 2537 2538 @classmethod 2539 def from_mpid_list(cls, mpid_list, api_key=None, endpoint=None): 2540 """ 2541 Build a DdbRobot from list of materials-project ids. 2542 2543 Args: 2544 mpid_list: List of Materials Project material_ids (e.g., ["mp-1234", "mp-1245"]). 2545 api_key (str): A String API key for accessing the MaterialsProject REST interface. 2546 If None, the code will check if there is a `PMG_MAPI_KEY` in your .pmgrc.yaml. 2547 endpoint (str): Url of endpoint to access the MaterialsProject REST interface. 2548 Defaults to the standard Materials Project REST address 2549 """ 2550 from abipy.core import restapi 2551 ddb_files = [] 2552 with restapi.get_mprester(api_key=api_key, endpoint=endpoint) as rest: 2553 for mpid in list_strings(mpid_list): 2554 try: 2555 ddb_string = rest._make_request("/materials/%s/abinit_ddb" % mpid) 2556 except rest.Error: 2557 cprint("Cannot get DDB for mp-id: %s, ignoring error" % mpid, "yellow") 2558 continue 2559 2560 _, tmpfile = tempfile.mkstemp(prefix=mpid, suffix='_DDB') 2561 ddb_files.append(tmpfile) 2562 with open(tmpfile, "wt") as fh: 2563 fh.write(ddb_string) 2564 2565 return cls.from_files(ddb_files) 2566 2567 #def get_qpoints_union(self): 2568 # """ 2569 # Return numpy array with the q-points in reduced coordinates found in the DDB files. 2570 # """ 2571 # qpoints = [] 2572 # for label, ddb in self.items(): 2573 # qpoints.extend(q.frac_coords for q in ddb.qpoints if q not in qpoints) 2574 2575 # return np.array(qpoints) 2576 2577 #def get_qpoints_intersection(self): 2578 # """Return numpy array with the q-points in reduced coordinates found in the DDB files.""" 2579 # qpoints = [] 2580 # for label, ddb in self.items(): 2581 # qpoints.extend(q.frac_coords for q in ddb.qpoints if q not in qpoints) 2582 # 2583 # return np.array(qpoints) 2584 2585 # DEBUGGING CODE (do not remove) 2586 #def find_duplicated_entries(self, std_tol=1e-5, verbose=1): 2587 # """ 2588 # Check for duplicated entries in the list of ddb files 2589 2590 # Args: 2591 # std_tol: Tolerance on standard deviation 2592 # verbose: Verbosity level. 2593 2594 # Return: (retcode, results) where results maps qpt --> DataFrame with perts as index. 2595 # """ 2596 # from pprint import pprint 2597 2598 # # Build q --> group of dataframes. 2599 # from collections import defaultdict 2600 # q2dfgroup = defaultdict(list) 2601 # for ddb in self.abifiles: 2602 # for qpt, df in ddb.computed_dynmat.items(): 2603 # q2dfgroup[qpt].append(df) 2604 2605 # retcode, results = 0, {} 2606 # for qpt, dfgroup in q2dfgroup.items(): 2607 # all_indset = [set(df.index) for df in dfgroup] 2608 # # Build union of all dynmat indices with this q 2609 # allps = set(all_indset[0]).union(*all_indset) 2610 # #allps = set(all_indset[0]).intersection(*all_indset) 2611 2612 # index, d_list = [], [] 2613 # for p in allps: 2614 # # Find dataframes with this p 2615 # found = [p in index for index in all_indset] 2616 # count = found.count(True) 2617 # if count == 1: continue 2618 # if verbose: 2619 # print("Found %s duplicated entries for p: %s" % (count, str(p))) 2620 2621 # # Compute stats for this p (complex numbers) 2622 # cvalues = [] 2623 # for f, df in zip(found, dfgroup): 2624 # if not f: continue 2625 # c = df["cvalue"].loc[[p]] 2626 # cvalues.append(c) 2627 2628 # cvalues = np.array(cvalues) 2629 # norms = np.abs(cvalues) 2630 # d = dict(mean=cvalues.mean(), std=cvalues.std(), 2631 # min_norm=norms.min(), max_norm=norms.max(), count=count) 2632 2633 # # Print warning if large deviation 2634 # #if d["max_norm"] - d["min_norm"] > 1e-5: 2635 # if d["std"] > std_tol: 2636 # retcode += 1 2637 # cprint("Found std > %s" % std_tol, "red") 2638 # pprint(cvalues) 2639 # if verbose: 2640 # pprint(d) 2641 # print(2 * "") 2642 2643 # d_list.append(d) 2644 # index.append(p) 2645 2646 # results[qpt] = pd.DataFrame(d_list, index=index) 2647 2648 # return retcode, results 2649 2650 def get_dataframe_at_qpoint(self, qpoint=None, units="eV", asr=2, chneut=1, dipdip=1, 2651 with_geo=True, with_spglib=True, abspath=False, funcs=None): 2652 """ 2653 Call anaddb to compute the phonon frequencies at a single q-point using the DDB files treated 2654 by the robot and the given anaddb input arguments. LO-TO splitting is not included. 2655 Build and return a |pandas-Dataframe| with results 2656 2657 Args: 2658 qpoint: Reduced coordinates of the qpoint where phonon modes are computed 2659 units: string specifying the units used for ph frequencies. Possible values in 2660 ("eV", "meV", "Ha", "cm-1", "Thz"). Case-insensitive. 2661 asr, chneut, dipdip: Anaddb input variable. See official documentation. 2662 with_geo: True if structure info should be added to the dataframe 2663 with_spglib: True to compute spglib space group and add it to the DataFrame. 2664 abspath: True if paths in index should be absolute. Default: Relative to getcwd(). 2665 funcs: Function or list of functions to execute to add more data to the DataFrame. 2666 Each function receives a |DdbFile| object and returns a tuple (key, value) 2667 where key is a string with the name of column and value is the value to be inserted. 2668 2669 Return: |pandas-DataFrame| 2670 """ 2671 # If qpoint is None, all the DDB must contain have the same q-point . 2672 if qpoint is None: 2673 if not all(len(ddb.qpoints) == 1 for ddb in self.abifiles): 2674 raise ValueError("Found more than one q-point in the DDB file. qpoint must be specified") 2675 2676 qpoint = self[0].qpoints[0] 2677 if any(np.any(ddb.qpoints[0] != qpoint) for ddb in self.abifiles): 2678 raise ValueError("All the q-points in the DDB files must be equal") 2679 2680 rows, row_names = [], [] 2681 for i, (label, ddb) in enumerate(self.items()): 2682 row_names.append(label) 2683 d = OrderedDict() 2684 #d = {aname: getattr(ddb, aname) for aname in attrs} 2685 #d.update({"qpgap": mdf.get_qpgap(spin, kpoint)}) 2686 2687 # Call anaddb to get the phonon frequencies. Note lo_to_splitting set to False. 2688 phbands = ddb.anaget_phmodes_at_qpoint(qpoint=qpoint, asr=asr, chneut=chneut, 2689 dipdip=dipdip, lo_to_splitting=False) 2690 # [nq, nmodes] array 2691 freqs = phbands.phfreqs[0, :] * phfactor_ev2units(units) 2692 2693 d.update({"mode" + str(i): freqs[i] for i in range(len(freqs))}) 2694 2695 # Add convergence parameters 2696 d.update(ddb.params) 2697 2698 # Add info on structure. 2699 if with_geo: 2700 d.update(phbands.structure.get_dict4pandas(with_spglib=with_spglib)) 2701 2702 # Execute functions. 2703 if funcs is not None: d.update(self._exec_funcs(funcs, ddb)) 2704 rows.append(d) 2705 2706 row_names = row_names if not abspath else self._to_relpaths(row_names) 2707 return pd.DataFrame(rows, index=row_names, columns=list(rows[0].keys())) 2708 2709 def anaget_phonon_plotters(self, **kwargs): 2710 r""" 2711 Invoke anaddb to compute phonon bands and DOS using the arguments passed via \*\*kwargs. 2712 Collect results and return `namedtuple` with the following attributes: 2713 2714 phbands_plotter: |PhononBandsPlotter| object. 2715 phdos_plotter: |PhononDosPlotter| object. 2716 """ 2717 # TODO: Multiprocessing? 2718 if "workdir" in kwargs: 2719 raise ValueError("Cannot specify `workdir` when multiple DDB file are executed.") 2720 2721 phbands_plotter, phdos_plotter = PhononBandsPlotter(), PhononDosPlotter() 2722 2723 for label, ddb in self.items(): 2724 # Invoke anaddb to get phonon bands and DOS. 2725 phbst_file, phdos_file = ddb.anaget_phbst_and_phdos_files(**kwargs) 2726 2727 def find_anaddb_ncpath(filepath): 2728 from abipy.flowtk.utils import Directory 2729 directory = Directory(os.path.dirname(filepath)) 2730 return directory.outpath_from_ext("anaddb.nc") 2731 2732 # Phonon frequencies with non analytical contributions, if calculated, are saved in anaddb.nc 2733 # Those results should be fetched from there and added to the phonon bands. 2734 # lo_to_splitting in ["automatic", True, False] and defaults to automatic. 2735 if kwargs.get("lo_to_splitting", False): 2736 #anaddb_path = os.path.join(os.path.dirname(phbst_file.filepath), "anaddb.nc") 2737 anaddb_path = find_anaddb_ncpath(phbst_file.filepath) 2738 phbst_file.phbands.read_non_anal_from_file(anaddb_path) 2739 2740 phbands_plotter.add_phbands(label, phbst_file, phdos=phdos_file) 2741 phbst_file.close() 2742 if phdos_file is not None: 2743 phdos_plotter.add_phdos(label, phdos=phdos_file.phdos) 2744 phdos_file.close() 2745 2746 return dict2namedtuple(phbands_plotter=phbands_plotter, phdos_plotter=phdos_plotter) 2747 2748 def anacompare_elastic(self, ddb_header_keys=None, with_structure=True, with_spglib=True, 2749 with_path=False, manager=None, verbose=0, **kwargs): 2750 """ 2751 Compute elastic and piezoelectric properties for all DDBs in the robot and build DataFrame. 2752 2753 Args: 2754 ddb_header_keys: List of keywords in the header of the DDB file 2755 whose value will be added to the Dataframe. 2756 with_structure: True to add structure parameters to the DataFrame. 2757 with_spglib: True to compute spglib space group and add it to the DataFrame. 2758 with_path: True to add DDB path to dataframe 2759 manager: |TaskManager| object. If None, the object is initialized from the configuration file 2760 verbose: verbosity level. Set it to a value > 0 to get more information 2761 kwargs: Keyword arguments passed to `ddb.anaget_elastic`. 2762 2763 Return: DataFrame and list of ElastData objects. 2764 """ 2765 ddb_header_keys = [] if ddb_header_keys is None else list_strings(ddb_header_keys) 2766 df_list, elastdata_list = [], [] 2767 for label, ddb in self.items(): 2768 # Invoke anaddb to compute elastic data. 2769 edata = ddb.anaget_elastic(verbose=verbose, manager=manager, **kwargs) 2770 elastdata_list.append(edata) 2771 2772 # Build daframe with properties derived from the elastic tensor. 2773 df = edata.get_elastic_properties_dataframe() 2774 2775 # Add metadata to the dataframe. 2776 df["formula"] = ddb.structure.formula 2777 for k in ddb_header_keys: 2778 df[k] = ddb.header[k] 2779 2780 # Add structural parameters to the dataframe. 2781 if with_structure: 2782 for skey, svalue in ddb.structure.get_dict4pandas(with_spglib=with_spglib).items(): 2783 df[skey] = svalue 2784 2785 # Add path to the DDB file. 2786 if with_path: df["ddb_path"] = ddb.filepath 2787 2788 df_list.append(df) 2789 2790 # Concatenate dataframes. 2791 return dict2namedtuple(df=pd.concat(df_list, ignore_index=True), 2792 elastdata_list=elastdata_list) 2793 2794 def anacompare_becs(self, ddb_header_keys=None, chneut=1, tol=1e-3, with_path=False, verbose=0): 2795 """ 2796 Compute Born effective charges for all DDBs in the robot and build DataFrame. 2797 with Voigt indices as columns + metadata. Useful for convergence studies. 2798 2799 Args: 2800 ddb_header_keys: List of keywords in the header of the DDB file 2801 whose value will be added to the Dataframe. 2802 chneut: Anaddb input variable. See official documentation. 2803 tol: Elements below this value are set to zero. 2804 with_path: True to add DDB path to dataframe 2805 verbose: verbosity level. Set it to a value > 0 to get more information 2806 2807 Return: ``namedtuple`` with the following attributes:: 2808 2809 df: DataFrame with Voigt as columns. 2810 becs_list: list of Becs objects. 2811 """ 2812 ddb_header_keys = [] if ddb_header_keys is None else list_strings(ddb_header_keys) 2813 df_list, becs_list = [], [] 2814 for label, ddb in self.items(): 2815 # Invoke anaddb to compute Becs 2816 _, becs = ddb.anaget_epsinf_and_becs(chneut=chneut, verbose=verbose) 2817 becs_list.append(becs) 2818 df = becs.get_voigt_dataframe(tol=tol) 2819 2820 # Add metadata to the dataframe. 2821 df["formula"] = ddb.structure.formula 2822 df["chneut"] = chneut 2823 for k in ddb_header_keys: 2824 df[k] = ddb.header[k] 2825 2826 # Add path to the DDB file. 2827 if with_path: df["ddb_path"] = ddb.filepath 2828 2829 df_list.append(df) 2830 2831 # Concatenate dataframes. 2832 return dict2namedtuple(df=pd.concat(df_list, ignore_index=True).sort_values(by="site_index"), 2833 becs_list=becs_list) 2834 2835 def anacompare_epsinf(self, ddb_header_keys=None, chneut=1, tol=1e-3, with_path=False, verbose=0): 2836 r""" 2837 Compute (eps^\inf) electronic dielectric tensor for all DDBs in the robot and build DataFrame. 2838 with Voigt indices as columns + metadata. Useful for convergence studies. 2839 2840 Args: 2841 ddb_header_keys: List of keywords in the header of the DDB file 2842 whose value will be added to the Dataframe. 2843 chneut: Anaddb input variable. See official documentation. 2844 tol: Elements below this value are set to zero. 2845 with_path: True to add DDB path to dataframe 2846 verbose: verbosity level. Set it to a value > 0 to get more information 2847 2848 Return: ``namedtuple`` with the following attributes:: 2849 2850 df: DataFrame with Voigt indices as columns. 2851 epsinf_list: List of |DielectricTensor| objects with eps^{inf} 2852 """ 2853 ddb_header_keys = [] if ddb_header_keys is None else list_strings(ddb_header_keys) 2854 df_list, epsinf_list = [], [] 2855 for label, ddb in self.items(): 2856 # Invoke anaddb to compute e_inf 2857 einf, _ = ddb.anaget_epsinf_and_becs(chneut=chneut, verbose=verbose) 2858 epsinf_list.append(einf) 2859 df = einf.get_voigt_dataframe(tol=tol) 2860 2861 # Add metadata to the dataframe. 2862 df["formula"] = ddb.structure.formula 2863 df["chneut"] = chneut 2864 for k in ddb_header_keys: 2865 df[k] = ddb.header[k] 2866 2867 # Add path to the DDB file. 2868 if with_path: df["ddb_path"] = ddb.filepath 2869 df_list.append(df) 2870 2871 # Concatenate dataframes. 2872 return dict2namedtuple(df=pd.concat(df_list, ignore_index=True), epsinf_list=epsinf_list) 2873 2874 def anacompare_eps0(self, ddb_header_keys=None, asr=2, chneut=1, tol=1e-3, with_path=False, verbose=0): 2875 """ 2876 Compute (eps^0) dielectric tensor for all DDBs in the robot and build DataFrame. 2877 with Voigt indices as columns + metadata. Useful for convergence studies. 2878 2879 Args: 2880 ddb_header_keys: List of keywords in the header of the DDB file 2881 whose value will be added to the Dataframe. 2882 asr, chneut, dipdip: Anaddb input variable. See official documentation. 2883 tol: Elements below this value are set to zero. 2884 with_path: True to add DDB path to dataframe 2885 verbose: verbosity level. Set it to a value > 0 to get more information 2886 2887 Return: ``namedtuple`` with the following attributes:: 2888 2889 df: DataFrame with Voigt as columns. 2890 eps0_list: List of |DielectricTensor| objects with eps^0. 2891 dgen_list: List of DielectricTensorGenerator. 2892 """ 2893 ddb_header_keys = [] if ddb_header_keys is None else list_strings(ddb_header_keys) 2894 df_list, eps0_list, dgen_list = [], [], [] 2895 for label, ddb in self.items(): 2896 # Invoke anaddb to compute e_0 2897 gen = ddb.anaget_dielectric_tensor_generator(asr=asr, chneut=chneut, dipdip=1, verbose=verbose) 2898 dgen_list.append(gen) 2899 eps0_list.append(gen.eps0) 2900 df = gen.eps0.get_voigt_dataframe(tol=tol) 2901 2902 # Add metadata to the dataframe. 2903 df["formula"] = ddb.structure.formula 2904 df["asr"] = asr 2905 df["chneut"] = chneut 2906 #df["dipdip"] = dipdip 2907 for k in ddb_header_keys: 2908 df[k] = ddb.header[k] 2909 2910 # Add path to the DDB file. 2911 if with_path: df["ddb_path"] = ddb.filepath 2912 2913 df_list.append(df) 2914 2915 # Concatenate dataframes. 2916 return dict2namedtuple(df=pd.concat(df_list, ignore_index=True), 2917 eps0_list=eps0_list, dgen_list=dgen_list) 2918 2919 def yield_figs(self, **kwargs): # pragma: no cover 2920 """ 2921 This function *generates* a predefined list of matplotlib figures with minimal input from the user. 2922 """ 2923 if all(ddb.has_at_least_one_atomic_perturbation() for ddb in self.abifiles): 2924 print("Invoking anaddb through anaget_phonon_plotters...") 2925 r = self.anaget_phonon_plotters() 2926 for fig in r.phbands_plotter.yield_figs(): yield fig 2927 for fig in r.phdos_plotter.yield_figs(): yield fig 2928 2929 def write_notebook(self, nbpath=None): 2930 """ 2931 Write a jupyter_ notebook to nbpath. If ``nbpath`` is None, a temporary file in the current 2932 working directory is created. Return path to the notebook. 2933 """ 2934 nbformat, nbv, nb = self.get_nbformat_nbv_nb(title=None) 2935 2936 anaget_phonon_plotters_kwargs = ("\n" 2937 '\tnqsmall=10, ndivsm=20, asr=2, chneut=1, dipdip=1, dos_method="tetra",\n' 2938 '\tlo_to_splitting=False, ngqpt=None, qptbounds=None,\n' 2939 '\tanaddb_kwargs=None, verbose=0') 2940 2941 args = [(l, f.filepath) for l, f in self.items()] 2942 nb.cells.extend([ 2943 #nbv.new_markdown_cell("# This is a markdown cell"), 2944 nbv.new_code_cell("robot = abilab.DdbRobot(*%s)\nrobot.trim_paths()\nrobot" % str(args)), 2945 nbv.new_code_cell("""#dfq = robot.get_dataframe_at_qpoint(qpoint=None, units="meV")"""), 2946 nbv.new_code_cell("r = robot.anaget_phonon_plotters(%s)" % anaget_phonon_plotters_kwargs), 2947 nbv.new_code_cell("r.phbands_plotter.get_phbands_frame()"), 2948 nbv.new_code_cell("r.phbands_plotter.ipw_select_plot()"), 2949 nbv.new_code_cell("r.phdos_plotter.ipw_select_plot()"), 2950 nbv.new_code_cell("r.phdos_plotter.ipw_harmonic_thermo()"), 2951 ]) 2952 2953 # Mixins 2954 nb.cells.extend(self.get_baserobot_code_cells()) 2955 2956 return self._write_nb_nbpath(nb, nbpath) 2957 2958 2959def get_2nd_ord_block_string(qpt, data): 2960 """ 2961 Helper function providing the lines required in a DDB file for a given 2962 q point and second order derivatives. 2963 2964 Args: 2965 qpt: the fractional coordinates of the q point. 2966 data: a dictionary of the form {qpt: {(idir1, ipert1, idir2, ipert2): complex value}} 2967 with the data that should be given in the string. 2968 2969 Returns: 2970 list of str: the lines that can be added to the DDB file. 2971 """ 2972 lines = [] 2973 lines.append(f" 2nd derivatives (non-stat.) - # elements :{len(data):8}") 2974 lines.append(" qpt{:16.8E}{:16.8E}{:16.8E} 1.0".format(*qpt)) 2975 l_format = "{:4d}" * 4 + " {:22.14E}" * 2 2976 for p, v in data.items(): 2977 lines.append(l_format.format(*p, v.real, v.imag)) 2978 2979 return lines 2980