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