#!/usr/bin/env python # vim: set fileencoding=utf-8 ts=8 sw=4 tw=0 : """ Convert the discrete CP2K PDOS points to a smoothed curve using convoluted gaussians. Also shifts the energies by the Fermi energy (so the Fermi energy will afterwards be at 0), and normalizes by the number of atoms of this kind. """ # Copyright (c) 2017 Tiziano Müller , # based on a Fortran tool written by Marcella Iannuzzi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import print_function import sys import re import argparse import contextlib import numpy as np HEADER_MATCH = re.compile( r'\# Projected DOS for atomic kind (?P\w+) at iteration step i = \d+, E\(Fermi\) = [ \t]* (?P[^\t ]+) a\.u\.') # Column indexes, starting from 0 EIGENVALUE_COLUMN = 1 DENSITY_COLUMN = 3 # from https://stackoverflow.com/a/17603000/1400465 @contextlib.contextmanager def smart_open(filename=None): if filename and filename != '-': fhandle = open(filename, 'w') else: fhandle = sys.stdout try: yield fhandle finally: if fhandle is not sys.stdout: fhandle.close() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('pdosfilenames', metavar='', type=str, nargs='+', help="the PDOS file generated by CP2K, specify two (alpha/beta) files for a common grid") parser.add_argument('--sigma', '-s', type=float, default=0.02, help="sigma for the gaussian distribution (default: 0.02)") parser.add_argument('--de', '-d', type=float, default=0.001, help="integration step size (default: 0.001)") parser.add_argument('--scale', '-c', type=float, default=1, help="scale the density by this factor (default: 1)") parser.add_argument('--total-sum', action='store_true', help="calculate and print the total sum for each orbital (default: no)") parser.add_argument('--no-header', action='store_true', default=False, help="do not print a header (default: print header)") parser.add_argument('--output', '-o', type=str, default=None, help="write output to specified file (default: write to standard output)") args = parser.parse_args() alldata = [] orb_headers = [] for pdosfilename in args.pdosfilenames: with open(pdosfilename, 'r') as fhandle: match = HEADER_MATCH.match(fhandle.readline().rstrip()) if not match: print(("The file '{}' does not look like a CP2K PDOS output.\n" "If it is indeed a correct output file, please report an issue at\n" " https://github.com/dev-zero/cp2k-tools/issues").format(pdosfilename)) sys.exit(1) efermi = float(match.group('Efermi')) # header is originally: ['#', 'MO', 'Eigenvalue', '[a.u.]', 'Occupation', 's', 'py', ...] header = fhandle.readline().rstrip().split()[1:] # remove the comment directly header[1:3] = [' '.join(header[1:3])] # rejoin "Eigenvalue" and its unit data = np.loadtxt(fhandle) # load the rest directly with numpy alldata.append(data) orb_headers += header[DENSITY_COLUMN:] # take the boundaries over all energy eigenvalues (not guaranteed to be the same) # add a margin to not cut-off Gaussians at the borders margin = 10 * args.sigma emin = min(np.min(data[:, EIGENVALUE_COLUMN]) for data in alldata) - margin emax = max(np.max(data[:, EIGENVALUE_COLUMN]) for data in alldata) + margin ncols = sum(data.shape[1] - DENSITY_COLUMN for data in alldata) nmesh = int((emax-emin)/args.de)+1 # calculate manually instead of using np.arange to ensure emax inside the mesh xmesh = np.linspace(emin, emax, nmesh) ymesh = np.zeros((nmesh, ncols)) # printing to stderr makes it possible to simply redirect the stdout to a file print("Integration step: {:14.5f}".format(args.de), file=sys.stderr) print("Sigma: {:14.5f}".format(args.sigma), file=sys.stderr) print("Minimum energy: {:14.5f} - {:.5f}".format(emin+margin, margin), file=sys.stderr) print("Maximum energy: {:14.5f} + {:.5f}".format(emax-margin, margin), file=sys.stderr) print("Nr of mesh points: {:14d}".format(nmesh), file=sys.stderr) fact = args.de/(args.sigma*np.sqrt(2.0*np.pi)) coloffset = 0 for fname, data in zip(args.pdosfilenames, alldata): print("Nr of lines: {:14d} in {}".format(data.shape[0], fname), file=sys.stderr) ncol = data.shape[1] - DENSITY_COLUMN for idx in range(nmesh): func = np.exp(-(xmesh[idx]-data[:, EIGENVALUE_COLUMN])**2/(2.0*args.sigma**2))*fact ymesh[idx, coloffset:(coloffset+ncol)] = func.dot(data[:, DENSITY_COLUMN:]) coloffset += ncol if args.total_sum: finalsum = np.sum(ymesh, 0)*args.de print("Sum over all meshpoints, per orbital:", file=sys.stderr) print(("{:16.8f}"*ncols).format(*finalsum), file=sys.stderr) xmesh -= efermi # put the Fermi energy at 0 xmesh *= 27.211384 # convert to eV ymesh *= args.scale # scale with smart_open(args.output) as fhandle: if not args.no_header: print(("{:>16}" + " {:>16}"*ncols).format("Energy_[eV]", *orb_headers), file=fhandle) for idx in range(nmesh): print(("{:16.8f}" + " {:16.8f}"*ncols).format(xmesh[idx], *ymesh[idx, :]), file=fhandle) if __name__ == '__main__': main() # vim: set ts=4 sw=4 tw=0 :