pymatgen.io.fiesta — pymatgen 2020.7.3 documentation

# Source code for pymatgen.io.fiesta

    # coding: utf-8

    """
    This module implements input and output for Fiesta (http://perso.neel.cnrs.fr/xavier.blase/fiesta/index.html).

    and

    -Nwchem2Fiesta class: to create the input files needed for a Fiesta run
    -Fiesta_run: run gw_fiesta and bse_fiesta
    -Localised Basis set reader
    """

    import re
    import os
    import shutil
    import subprocess

    from string import Template
    from monty.io import zopen
    from monty.json import MSONable

    from pymatgen.core.structure import Molecule

    __author__ = 'ndardenne'
    __copyright__ = "Copyright 2012, The Materials Project"
    __version__ = "0.1"
    __email__ = "n.dardenne@uclouvain.be"
    __date__ = "24/5/15"

    class Nwchem2Fiesta(MSONable):
        """
        To run NWCHEM2FIESTA inside python:

        If nwchem.nw is the input, nwchem.out the output, and structure.movecs the
        "movecs" file, the syntax to run NWCHEM2FIESTA is: NWCHEM2FIESTA
        nwchem.nw  nwchem.nwout  structure.movecs > log_n2f
        """

        def __init__(self, folder, filename="nwchem", log_file="log_n2f"):
            """
            folder: where are stored the nwchem
            filename: name of nwchem files read by NWCHEM2FIESTA (filename.nw, filename.nwout and filename.movecs)
            logfile: logfile of NWCHEM2FIESTA

            the run method launchs NWCHEM2FIESTA

            """

            self.folder = folder
            self.filename = filename
            self.log_file = log_file

            self._NWCHEM2FIESTA_cmd = "NWCHEM2FIESTA"
            self._nwcheminput_fn = filename + ".nw"
            self._nwchemoutput_fn = filename + ".nwout"
            self._nwchemmovecs_fn = filename + ".movecs"

        def run(self):
            """
            Performs actual NWCHEM2FIESTA run
            """

            init_folder = os.getcwd()
            os.chdir(self.folder)

            with zopen(self.log_file, 'w') as fout:
                subprocess.call([self._NWCHEM2FIESTA_cmd, self._nwcheminput_fn,
                                 self._nwchemoutput_fn, self._nwchemmovecs_fn],
                                stdout=fout)

            os.chdir(init_folder)

        def as_dict(self):
            """
            :return: MSONable dict
            """
            return {"@module": self.__class__.__module__,
                    "@class": self.__class__.__name__,
                    "filename": self.filename,
                    "folder": self.folder}

        @classmethod
        def from_dict(cls, d):
            """
            :param d: Dict representation.
            :return: Nwchem2Fiesta
            """
            return Nwchem2Fiesta(folder=d["folder"], filename=d["filename"])

    class FiestaRun(MSONable):
        """
        To run FIESTA inside python:
            if grid is [x,x] then bse runs
            if grid is [x,x,y] the fiesta(gw) runs
            otherwise it breaks
        """

        def __init__(self, folder=os.getcwd(), grid=[2, 2, 2], log_file="log"):
            """
            Args:
                folder: Folder to look for runs.
                grid:
                log_file: logfile of Fiesta
            """
            self.folder = folder
            self.log_file = log_file
            self.grid = grid

        def run(self):
            """
            Performs FIESTA (gw) run.
            """
            if len(self.grid) == 3:
                self.mpi_procs = self.grid[0] * self.grid[1] * self.grid[2]
                self._gw_run()
            elif len(self.grid) == 2:
                self.mpi_procs = self.grid[0] * self.grid[1]
                self.bse_run()
            else:
                raise ValueError(
                    "Wrong grid size: must be [nrow, ncolumn, nslice] for gw of [nrow, nslice] for bse")

        def _gw_run(self):
            """
            Performs FIESTA (gw) run
            """

            if self.folder != os.getcwd():
                init_folder = os.getcwd()
                os.chdir(self.folder)

            with zopen(self.log_file, 'w') as fout:
                subprocess.call(["mpirun", "-n", str(self.mpi_procs), "fiesta",
                                 str(self.grid[0]), str(self.grid[1]),
                                 str(self.grid[2])], stdout=fout)

            if self.folder != os.getcwd():
                os.chdir(init_folder)

        def bse_run(self):
            """
            Performs BSE run
            """

            if self.folder != os.getcwd():
                init_folder = os.getcwd()
                os.chdir(self.folder)

            with zopen(self.log_file, 'w') as fout:
                subprocess.call(
                    ["mpirun", "-n", str(self.mpi_procs), "bse", str(self.grid[0]),
                     str(self.grid[1])], stdout=fout)

            if self.folder != os.getcwd():
                os.chdir(init_folder)

        def as_dict(self):
            """
            :return: MSONable dict
            """
            return {"@module": self.__class__.__module__,
                    "@class": self.__class__.__name__,
                    "log_file": self.log_file,
                    "grid": self.grid,
                    "folder": self.folder}

        @classmethod
        def from_dict(cls, d):
            """
            :param d: Dict representation
            :return: FiestaRun
            """
            return FiestaRun(folder=d["folder"], grid=d["grid"],
                             log_file=d['log_file'])

    class BasisSetReader:
        """
        A basis set reader.
        Basis set are stored in data as a dict:
        :key l_zeta_ng for each nl orbitals which contain list of tuple (alpha, coef) for each of the ng gaussians
        in l_zeta orbital
        """

        def __init__(self, filename):
            """
            Args:
                filename: Filename to read.
            """
            self.filename = filename

            with zopen(filename) as f:
                basis_set = f.read()

            self.data = self._parse_file(basis_set)
            # compute the number of nlm orbitals per atom
            self.data.update(n_nlmo=self.set_n_nlmo())

        def _parse_file(self, input):

            lmax_nnlo_patt = re.compile(r"\s* (\d+) \s+ (\d+) \s+ \# .* ",
                                        re.VERBOSE)

            nl_orbital_patt = re.compile(r"\s* (\d+) \s+ (\d+) \s+ (\d+) \s+ \# .* ",
                                         re.VERBOSE)

            coef_alpha_patt = re.compile(r"\s* ([-\d.\D]+) \s+ ([-\d.\D]+) \s* ",
                                         re.VERBOSE)

            preamble = []
            basis_set = {}
            parse_preamble = False
            parse_lmax_nnlo = False
            parse_nl_orbital = False

            for line in input.split("\n"):

                if parse_nl_orbital:
                    m = nl_orbital_patt.search(line)
                    n = coef_alpha_patt.search(line)
                    if m:
                        l = m.group(1)
                        zeta = m.group(2)
                        ng = m.group(3)
                        basis_set[l + "_" + zeta + "_" + ng] = []
                    elif n:
                        alpha = n.group(1)
                        coef = n.group(2)
                        basis_set[l + "_" + zeta + "_" + ng].append((alpha, coef))
                elif parse_lmax_nnlo:
                    m = lmax_nnlo_patt.search(line)
                    if m:
                        lmax = m.group(1)
                        nnlo = m.group(2)
                        parse_lmax_nnlo = False
                        parse_nl_orbital = True
                elif parse_preamble:
                    preamble.append(line.strip())

                if line.find("</preamble>") != -1:
                    parse_preamble = False
                    parse_lmax_nnlo = True
                elif line.find("<preamble>") != -1:
                    parse_preamble = True

            basis_set.update(lmax=lmax, n_nlo=nnlo, preamble=preamble)
            return basis_set

        def set_n_nlmo(self):
            """
            :return: the number of nlm orbitals for the basis set
            """

            nnlmo = 0

            data_tmp = self.data
            data_tmp.pop('lmax')
            data_tmp.pop('n_nlo')
            data_tmp.pop('preamble')

            for l_zeta_ng in data_tmp:
                l = l_zeta_ng.split("_")[0]
                nnlmo = nnlmo + (2 * int(l) + 1)

            return str(nnlmo)

        def infos_on_basis_set(self):
            """
            infos on the basis set as in Fiesta log
            """
            o = []
            o.append("=========================================")
            o.append("Reading basis set:")
            o.append("")
            o.append(" Basis set for {} atom ".format(str(self.filename)))
            o.append(" Maximum angular momentum = {}".format(self.data['lmax']))
            o.append(" Number of atomics orbitals = {}".format(self.data['n_nlo']))
            o.append(" Number of nlm orbitals = {}".format(self.data['n_nlmo']))
            o.append("=========================================")

            return str(0)

    class FiestaInput(MSONable):
        """
        Input File for Fiesta called "cell.in" by default (mandatory in Fiesta for now)
        """

        def __init__(self, mol,
                     correlation_grid={'dE_grid': u'0.500', 'n_grid': u'14'},
                     Exc_DFT_option={'rdVxcpsi': u'1'},
                     COHSEX_options={'eigMethod': u'C', 'mix_cohsex': u'0.500',
                                     'nc_cohsex': u'0', 'nit_cohsex': u'0',
                                     'nv_cohsex': u'0', 'resMethod': u'V',
                                     'scf_cohsex_wf': u'0'},
                     GW_options={'nc_corr': u'10', 'nit_gw': u'3',
                                 'nv_corr': u'10'},
                     BSE_TDDFT_options={'do_bse': u'1', 'do_tddft': u'0',
                                        'nc_bse': u'382', 'nit_bse': u'50',
                                        'npsi_bse': u'1', 'nv_bse': u'21'}):
            """
            :param mol: pymatgen mol
            :param correlation_grid: dict
            :param Exc_DFT_option: dict
            :param COHSEX_options: dict
            :param GW_options: dict
            :param BSE_TDDFT_options: dict
            """

            self._mol = mol
            self.correlation_grid = correlation_grid
            self.Exc_DFT_option = Exc_DFT_option
            self.COHSEX_options = COHSEX_options
            self.GW_options = GW_options
            self.BSE_TDDFT_options = BSE_TDDFT_options

        def set_auxiliary_basis_set(self, folder, auxiliary_folder,
                                    auxiliary_basis_set_type="aug_cc_pvtz"):
            """
            copy in the desired folder the needed auxiliary basis set "X2.ion" where X is a specie.
            :param auxiliary_folder: folder where the auxiliary basis sets are stored
            :param auxiliary_basis_set_type: type of basis set (string to be found in the extension of the file name; must
                be in lower case). ex: C2.ion_aug_cc_pvtz_RI_Weigend find "aug_cc_pvtz"
            """

            list_files = os.listdir(auxiliary_folder)

            for specie in self._mol.symbol_set:
                for file in list_files:
                    if file.upper().find(
                            specie.upper() + "2") != -1 and file.lower().find(auxiliary_basis_set_type) != -1:
                        shutil.copyfile(auxiliary_folder + "/" + file,
                                        folder + "/" + specie + "2.ion")

        def set_GW_options(self, nv_band=10, nc_band=10, n_iteration=5, n_grid=6,
                           dE_grid=0.5):
            """
            Set parameters in cell.in for a GW computation
            :param nv__band: number of valence bands to correct with GW
            :param nc_band: number of conduction bands to correct with GW
            :param n_iteration: number of iteration
            :param n_grid and dE_grid:: number of points and spacing in eV for correlation grid
            """

            self.GW_options.update(nv_corr=nv_band, nc_corr=nc_band,
                                   nit_gw=n_iteration)
            self.correlation_grid.update(dE_grid=dE_grid, n_grid=n_grid)

        def make_FULL_BSE_Densities_folder(self, folder):
            """
            mkdir "FULL_BSE_Densities" folder (needed for bse run) in the desired folder
            """

            if os.path.exists(folder + "/FULL_BSE_Densities"):
                return "FULL_BSE_Densities folder already exists"
            else:
                os.makedirs(folder + "/FULL_BSE_Densities")
                return "makedirs FULL_BSE_Densities folder"

        def set_BSE_options(self, n_excitations=10, nit_bse=200):
            """
            Set parameters in cell.in for a BSE computation
            :param nv_bse: number of valence bands
            :param nc_bse: number of conduction bands
            :param n_excitations: number of excitations
            :param nit_bse: number of iterations
            """

            self.BSE_TDDFT_options.update(npsi_bse=n_excitations, nit_bse=nit_bse)

        def dump_BSE_data_in_GW_run(self, BSE_dump=True):
            """
            :param BSE_dump: boolean
            :return: set the "do_bse" variable to one in cell.in
            """

            if BSE_dump:
                self.BSE_TDDFT_options.update(do_bse=1, do_tddft=0)
            else:
                self.BSE_TDDFT_options.update(do_bse=0, do_tddft=0)

        def dump_TDDFT_data_in_GW_run(self, TDDFT_dump=True):
            """
            :param TDDFT_dump: boolen
            :return: set the do_tddft variable to one in cell.in
            """
            if TDDFT_dump:
                self.BSE_TDDFT_options.update(do_bse=0, do_tddft=1)
            else:
                self.BSE_TDDFT_options.update(do_bse=0, do_tddft=0)

        @property
        def infos_on_system(self):
            """
            Returns infos on initial parameters as in the log file of Fiesta
            """

            o = []
            o.append("=========================================")
            o.append("Reading infos on system:")
            o.append("")
            o.append(" Number of atoms = {} ; number of species = {}".format(
                int(self._mol.composition.num_atoms), len(self._mol.symbol_set)))
            o.append(" Number of valence bands = {}".format(
                int(self._mol.nelectrons / 2)))
            o.append(" Sigma grid specs: n_grid = {} ;  dE_grid = {} (eV)".format(
                self.correlation_grid['n_grid'], self.correlation_grid['dE_grid']))
            if int(self.Exc_DFT_option['rdVxcpsi']) == 1:
                o.append(" Exchange and correlation energy read from Vxcpsi.mat")
            elif int(self.Exc_DFT_option['rdVxcpsi']) == 0:
                o.append(" Exchange and correlation energy re-computed")

            if self.COHSEX_options['eigMethod'] == "C":
                o.append(
                    " Correcting  {} valence bands and   {} conduction bands at COHSEX level".format(
                        self.COHSEX_options['nv_cohsex'],
                        self.COHSEX_options['nc_cohsex']))
                o.append(" Performing   {} diagonal COHSEX iterations".format(
                    self.COHSEX_options['nit_cohsex']))
            elif self.COHSEX_options['eigMethod'] == "HF":
                o.append(
                    " Correcting  {} valence bands and   {} conduction bands at HF level".format(
                        self.COHSEX_options['nv_cohsex'],
                        self.COHSEX_options['nc_cohsex']))
                o.append(" Performing   {} diagonal HF iterations".format(
                    self.COHSEX_options['nit_cohsex']))

            o.append(" Using resolution of identity : {}".format(
                self.COHSEX_options['resMethod']))
            o.append(
                " Correcting  {} valence bands and  {} conduction bands at GW level".format(
                    self.GW_options['nv_corr'], self.GW_options['nc_corr']))
            o.append(
                " Performing   {} GW iterations".format(self.GW_options['nit_gw']))

            if int(self.BSE_TDDFT_options['do_bse']) == 1:
                o.append(" Dumping data for BSE treatment")

            if int(self.BSE_TDDFT_options['do_tddft']) == 1:
                o.append(" Dumping data for TD-DFT treatment")
            o.append("")
            o.append(" Atoms in cell cartesian A:")
            symbols = []
            for syb in self._mol.symbol_set:
                symbols.append(syb)

            for site in self._mol:
                o.append(" {} {} {} {}".format(site.x, site.y,
                                               site.z, int(symbols.index(site.specie.symbol)) + 1))

            o.append("=========================================")

            return str(o)

        @property
        def molecule(self):
            """
            Returns molecule associated with this FiestaInput.
            """
            return self._mol

        def __str__(self):

            symbols = []
            for syb in self._mol.symbol_set:
                symbols.append(syb)

            geometry = []
            for site in self._mol:
                geometry.append(" {} {} {} {}".format(site.x, site.y,
                                                      site.z, int(symbols.index(site.specie.symbol)) + 1))

            t = Template("""# number of atoms and species
       $nat    $nsp
    # number of valence bands
        $nvbands
    # number of points and spacing in eV for correlation grid
        $n_grid    $dE_grid
    # relire=1 ou recalculer=0 Exc DFT
        $rdVxcpsi
    # number of COHSEX corrected occp and unoccp bands: C=COHSEX  H=HF
        $nv_cohsex    $nc_cohsex   $eigMethod
    # number of COHSEX iter, scf on wfns, mixing coeff; V=RI-V  I=RI-D
        $nit_cohsex   $resMethod       $scf_cohsex_wf       $mix_cohsex
    # number of GW corrected occp and unoccp bands
       $nv_corr   $nc_corr
    # number of GW iterations
        $nit_gw
    # dumping for BSE and TDDFT
        $do_bse    $do_tddft
    # number of occp. and virtual bands fo BSE: nocore and up to 40 eVs
        $nv_bse   $nc_bse
    # number of excitations needed and number of iterations
        $npsi_bse   $nit_bse
    # list of symbols in order
    $symbols
    # scaling factor
        1.000
    # atoms x,y,z cartesian .. will be multiplied by scale
    $geometry
                """)

            return t.substitute(nat=int(self._mol.composition.num_atoms),
                                nsp=len(self._mol.symbol_set),
                                nvbands=int(self._mol.nelectrons / 2),
                                n_grid=self.correlation_grid['n_grid'],
                                dE_grid=self.correlation_grid['dE_grid'],
                                rdVxcpsi=self.Exc_DFT_option['rdVxcpsi'],
                                nv_cohsex=self.COHSEX_options['nv_cohsex'],
                                nc_cohsex=self.COHSEX_options['nc_cohsex'],
                                eigMethod=self.COHSEX_options['eigMethod'],
                                nit_cohsex=self.COHSEX_options['nit_cohsex'],
                                resMethod=self.COHSEX_options['resMethod'],
                                scf_cohsex_wf=self.COHSEX_options['scf_cohsex_wf'],
                                mix_cohsex=self.COHSEX_options['mix_cohsex'],
                                nv_corr=self.GW_options['nv_corr'],
                                nc_corr=self.GW_options['nc_corr'],
                                nit_gw=self.GW_options['nit_gw'],
                                do_bse=self.BSE_TDDFT_options['do_bse'],
                                do_tddft=self.BSE_TDDFT_options['do_tddft'],
                                nv_bse=self.BSE_TDDFT_options['nv_bse'],
                                nc_bse=self.BSE_TDDFT_options['nc_bse'],
                                npsi_bse=self.BSE_TDDFT_options['npsi_bse'],
                                nit_bse=self.BSE_TDDFT_options['nit_bse'],
                                symbols="\n".join(symbols),
                                geometry="\n".join(geometry))

        def write_file(self, filename):
            """
            Write FiestaInput to a file
            :param filename: Filename
            """
            with zopen(filename, "w") as f:
                f.write(self.__str__())

        def as_dict(self):
            """
            :return: MSONable dict
            """
            return {
                "mol": self._mol.as_dict(),
                "correlation_grid": self.correlation_grid,
                "Exc_DFT_option": self.Exc_DFT_option,
                "COHSEX_options": self.COHSEX_options,
                "GW_options": self.GW_options,
                "BSE_TDDFT_options": self.BSE_TDDFT_options
            }

        @classmethod
        def from_dict(cls, d):
            """
            :param d: Dict representation
            :return: FiestaInput
            """
            return FiestaInput(Molecule.from_dict(d["mol"]),
                               correlation_grid=d["correlation_grid"],
                               Exc_DFT_option=d["Exc_DFT_option"],
                               COHSEX_options=d["geometry_options"],
                               GW_options=d["symmetry_options"],
                               BSE_TDDFT_options=d["memory_options"])

        @classmethod
        def from_string(cls, string_input):
            """
            Read an FiestaInput from a string. Currently tested to work with
            files generated from this class itself.

            Args:
                string_input: string_input to parse.
            Returns:
                FiestaInput object
            """

            correlation_grid = {}
            Exc_DFT_option = {}
            COHSEX_options = {}
            GW_options = {}
            BSE_TDDFT_options = {}

            lines = string_input.strip().split("\n")

            # number of atoms and species
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            nat = toks[0]
            nsp = toks[1]
            # number of valence bands
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()

            # correlation_grid
            # number of points and spacing in eV for correlation grid
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            correlation_grid['n_grid'] = toks[0]
            correlation_grid['dE_grid'] = toks[1]

            # Exc DFT
            # relire=1 ou recalculer=0 Exc DFT
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            Exc_DFT_option['rdVxcpsi'] = toks[0]

            # COHSEX
            # number of COHSEX corrected occp and unoccp bands: C=COHSEX  H=HF
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            COHSEX_options['nv_cohsex'] = toks[0]
            COHSEX_options['nc_cohsex'] = toks[1]
            COHSEX_options['eigMethod'] = toks[2]
            # number of COHSEX iter, scf on wfns, mixing coeff; V=RI-V  I=RI-D
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            COHSEX_options['nit_cohsex'] = toks[0]
            COHSEX_options['resMethod'] = toks[1]
            COHSEX_options['scf_cohsex_wf'] = toks[2]
            COHSEX_options['mix_cohsex'] = toks[3]

            # GW
            # number of GW corrected occp and unoccp bands
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            GW_options['nv_corr'] = toks[0]
            GW_options['nc_corr'] = toks[1]
            # number of GW iterations
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            GW_options['nit_gw'] = toks[0]

            # BSE
            # dumping for BSE and TDDFT
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            BSE_TDDFT_options['do_bse'] = toks[0]
            BSE_TDDFT_options['do_tddft'] = toks[1]
            # number of occp. and virtual bands fo BSE: nocore and up to 40 eVs
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            BSE_TDDFT_options['nv_bse'] = toks[0]
            BSE_TDDFT_options['nc_bse'] = toks[1]
            # number of excitations needed and number of iterations
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            BSE_TDDFT_options['npsi_bse'] = toks[0]
            BSE_TDDFT_options['nit_bse'] = toks[1]

            # Molecule
            # list of symbols in order
            lines.pop(0)
            atname = []
            i = int(nsp)
            while i != 0:
                l = lines.pop(0).strip()
                toks = l.split()
                atname.append(toks[0])
                i -= 1

            # scaling factor
            lines.pop(0)
            l = lines.pop(0).strip()
            toks = l.split()
            # atoms x,y,z cartesian .. will be multiplied by scale
            lines.pop(0)
            # Parse geometry
            species = []
            coords = []
            i = int(nat)
            while i != 0:
                l = lines.pop(0).strip()
                toks = l.split()
                coords.append([float(j) for j in toks[0:3]])
                species.append(atname[int(toks[3]) - 1])
                i -= 1

            mol = Molecule(species, coords)

            return FiestaInput(mol=mol, correlation_grid=correlation_grid,
                               Exc_DFT_option=Exc_DFT_option,
                               COHSEX_options=COHSEX_options,
                               GW_options=GW_options,
                               BSE_TDDFT_options=BSE_TDDFT_options)

        @classmethod
        def from_file(cls, filename):
            """
            Read an Fiesta input from a file. Currently tested to work with
            files generated from this class itself.

            Args:
                filename: Filename to parse.

            Returns:
                FiestaInput object
            """
            with zopen(filename) as f:
                return cls.from_string(f.read())

    class FiestaOutput:
        """
        A Fiesta output file parser.

        All energies are in eV.
        """

        def __init__(self, filename):
            """
            Args:
                filename: Filename to read.
            """
            self.filename = filename

            with zopen(filename) as f:
                data = f.read()

            chunks = re.split(r"GW Driver iteration", data)

            # preamble: everything before the first GW Driver iteration
            chunks.pop(0)

            # self.job_info = self._parse_preamble(preamble)
            self.data = [self._parse_job(c) for c in chunks]

        def _parse_job(self, output):

            GW_BANDS_results_patt = re.compile(
                r"^<it.*  \| \s+ (\D+\d*) \s+ \| \s+ ([-\d.]+) \s+ ([-\d.]+) \s+ ([-\d.]+) \s+ \| "
                r" \s+ ([-\d.]+) \s+ ([-\d.]+) \s+ ([-\d.]+) \s+ \|"
                r" \s+ ([-\d.]+) \s+ ([-\d.]+) \s+ ", re.VERBOSE)

            GW_GAPS_results_patt = re.compile(
                r"^<it.*  \| \s+ Egap_KS \s+ = \s+ ([-\d.]+) \s+ \| \s+ Egap_QP \s+ = \s+ ([-\d.]+) \s+ \| "
                r" \s+ Egap_QP \s+ = \s+ ([-\d.]+) \s+ \|", re.VERBOSE)

            end_patt = re.compile(r"\s*program returned normally\s*")

            total_time_patt = re.compile(r"\s*total \s+ time: \s+  ([\d.]+) .*",
                                         re.VERBOSE)

            GW_results = {}
            parse_gw_results = False
            parse_total_time = False

            for l in output.split("\n"):

                if parse_total_time:
                    m = end_patt.search(l)
                    if m:
                        GW_results.update(end_normally=True)

                    m = total_time_patt.search(l)
                    if m:
                        GW_results.update(total_time=m.group(1))

                if parse_gw_results:
                    if l.find("Dumping eigen energies") != -1:
                        parse_total_time = True
                        parse_gw_results = False
                        continue
                    else:
                        m = GW_BANDS_results_patt.search(l)
                        if m:
                            d = {}
                            d.update(band=m.group(1).strip(), eKS=m.group(2),
                                     eXX=m.group(3), eQP_old=m.group(4),
                                     z=m.group(5), sigma_c_Linear=m.group(6),
                                     eQP_Linear=m.group(7),
                                     sigma_c_SCF=m.group(8), eQP_SCF=m.group(9))
                            GW_results[m.group(1).strip()] = d

                        n = GW_GAPS_results_patt.search(l)
                        if n:
                            d = {}
                            d.update(Egap_KS=n.group(1), Egap_QP_Linear=n.group(2),
                                     Egap_QP_SCF=n.group(3))
                            GW_results["Gaps"] = d

                if l.find("GW Results") != -1:
                    parse_gw_results = True

            return GW_results

    class BSEOutput:
        """
        A bse output file parser. The start...

        All energies are in eV.
        """

        def __init__(self, filename):
            """
            Args:
                filename: Filename to read.
            """
            self.filename = filename

            with zopen(filename) as f:
                log_bse = f.read()

            # self.job_info = self._parse_preamble(preamble)
            self.exiton = self._parse_job(log_bse)

        def _parse_job(self, output):

            BSE_exitons_patt = re.compile(
                r"^exiton \s+ (\d+)  : \s+  ([\d.]+) \( \s+ ([-\d.]+) \) \s+ \| .*  ",
                re.VERBOSE)

            end_patt = re.compile(r"\s*program returned normally\s*")

            total_time_patt = re.compile(r"\s*total \s+ time: \s+  ([\d.]+) .*",
                                         re.VERBOSE)

            BSE_results = {}
            parse_BSE_results = False
            parse_total_time = False

            for l in output.split("\n"):

                if parse_total_time:
                    m = end_patt.search(l)
                    if m:
                        BSE_results.update(end_normally=True)

                    m = total_time_patt.search(l)
                    if m:
                        BSE_results.update(total_time=m.group(1))

                if parse_BSE_results:
                    if l.find(
                            "FULL BSE main valence -> conduction transitions weight:") != -1:
                        parse_total_time = True
                        parse_BSE_results = False
                        continue
                    else:
                        m = BSE_exitons_patt.search(l)
                        if m:
                            d = {}
                            d.update(bse_eig=m.group(2), osc_strength=m.group(3))
                            BSE_results[str(m.group(1).strip())] = d

                if l.find("FULL BSE eig.(eV), osc. strength and dipoles:") != -1:
                    parse_BSE_results = True

            return BSE_results