"""
Run DFTB+ with plams
contributed by Patrick Melix
based on code from Michal Handzlik
see documentation for an example
"""
from os.path import join as opj
from scm.plams.core.basejob import SingleJob
from scm.plams.core.errors import MoleculeError
from scm.plams.core.results import Results
from scm.plams.core.settings import Settings
from scm.plams.mol.molecule import Molecule
from scm.plams.tools.units import Units
__all__ = ["DFTBPlusJob", "DFTBPlusResults"]
[docs]class DFTBPlusResults(Results):
"""A Class for handling DFTB+ Results."""
_outfile = "detailed.out"
_xyzout = "geo_end.xyz"
_genout = "geo_end.gen"
[docs] def get_molecule(self):
"""get_molecule()
Return the molecule from the 'geo_end.gen' file. If there is an Error, try to read the 'geo_end.xyz' file.
"""
try:
# The .gen file contains the cell, ASE can read it
mol = Molecule(self[self._genout], inputformat="ase")
except MoleculeError:
# Fallback if no ASE found
mol = Molecule(filename=self[self._xyzout])
except:
mol = Molecule()
return mol
[docs] def get_energy(self, string="Total energy", unit="au"):
"""get_energy(string='Total energy', unit='au')
Return the energy given in the output with the description *string*, expressed in *unit*. Defaults to ``Total energy`` and ``au``.
"""
try:
energy = float(self.grep_file(self._outfile, pattern=string + ":")[0].split()[2])
energy = Units.convert(energy, "au", unit)
except:
energy = float("nan")
return energy
[docs] def get_atomic_charges(self):
"""get_atomic_charges()
Returns a dictonary with atom numbers and their charges, ordering is the same as in the input.
"""
try:
atomic_charges = {}
string = self.awk_file(
self._outfile, script="/Net atomic charges/{do_print=1} NF==0 {do_print=0 }do_print==1 {print}"
)
for line in string:
if string[0] == line or string[1] == line:
continue
l = line.split()
atomic_charges[l[0]] = float(l[1])
except:
atomic_charges = {}
return atomic_charges
[docs]class DFTBPlusJob(SingleJob):
"""A class representing a single computational job with DFTB+.
Only supports molecular coordinates, no support for lattice yet."""
_result_type = DFTBPlusResults
_filenames = {"inp": "dftb_in.hsd", "run": "$JN.run", "out": "$JN.out", "err": "$JN.err", "gen": "$JN.gen"}
def _parsemol(self):
# use ASE to write molecule if available
if "ase" in Molecule._writeformat:
filename = opj(self.path, self._filename("gen"))
self.molecule.write(filename, outputformat="ase", format="gen")
self.settings.input.geometry._h = "GenFormat"
self.settings.input.geometry._1 = "<<< " + self._filename("gen")
else:
# Old way of handling gen-format ourselves, delete if ASE becomes obligatory
atom_types = {}
n = 1
atoms_line = ""
for atom in self.molecule:
if atom.symbol not in atom_types:
atoms_line += atom.symbol + " "
atom_types[atom.symbol] = n
n += 1
# check PBC
lattice = []
geomType = "C"
for vec in self.molecule.lattice:
if not all(isinstance(x, (int, float)) for x in vec):
raise ValueError("Non-Number in Lattice Vectors, not compatible with DFTBPlus")
lattice.append(vec)
geomType = "S"
self.settings.input.geometry._h = "GenFormat"
self.settings.input.geometry._1 = "%i %s" % (len(self.molecule), geomType)
self.settings.input.geometry._2 = atoms_line
self.settings.input.geometry._3 = ""
for i, atom in enumerate(self.molecule):
self.settings.input.geometry["_" + str(i + 4)] = ("%5i" % (i + 1)) + atom.str(
symbol=str(atom_types[atom.symbol])
)
if len(vec) > 0:
j = i + 1
# origin
self.settings.input.geometry["_" + str(j + 4)] = "0.0 0.0 0.0"
j += 1
for i, vec in enumerate(lattice):
self.settings.input.geometry["_" + str(i + j + 4)] = "%f %f %f" % (vec)
def _removemol(self):
if "geometry" in self.settings.input:
del self.settings.input.geometry
[docs] def get_runscript(self):
"""dftb+ has to be in your $PATH!"""
ret = "dftb+ "
if self.settings.runscript.stdout_redirect:
ret += " >" + self._filename("out")
ret += "\n\n"
return ret
[docs] def check(self):
"""Returns true if 'ERROR!' is not found in the output."""
s = self.results.grep_output("ERROR!")
return len(s) == 0