"""
The vasprun.xml parser interface.
---------------------------------
Contains the parsing interfaces to ``parsevasp`` used to parse ``vasprun.xml`` content.
"""
# pylint: disable=abstract-method, too-many-public-methods
import numpy as np
from parsevasp import constants as parsevaspct
from parsevasp.vasprun import Xml
from aiida_vasp.parsers.content_parsers.base import BaseFileParser
from aiida_vasp.utils.compare_bands import get_band_properties
[docs]
class VasprunParser(BaseFileParser):
"""The parser interface that enables parsing of ``vasprun.xml`` content.
The parser is triggered by using the keys listed in ``PARSABLE_QUANTITIES``.
"""
OPEN_MODE = 'rb'
DEFAULT_SETTINGS = {
'quantities_to_parse': [
'structure',
'eigenvalues',
'dos',
'kpoints',
'occupancies',
'trajectory',
'energies',
'projectors',
'dielectrics',
'born_charges',
'hessian',
'dynmat',
'forces',
'stress',
'total_energies',
'maximum_force',
'maximum_stress',
'band_properties',
'version',
],
'energy_type': ['energy_extrapolated'],
'electronic_step_energies':
False
}
PARSABLE_QUANTITIES = {
'structure': {
'inputs': [],
'name': 'structure',
'prerequisites': [],
'alternatives': ['poscar-structure']
},
'eigenvalues': {
'inputs': [],
'name': 'eigenvalues',
'prerequisites': [],
'alternatives': ['eigenval-eigenvalues']
},
'dos': {
'inputs': [],
'name': 'dos',
'prerequisites': [],
'alternatives': ['doscar-dos']
},
'kpoints': {
'inputs': [],
'name': 'kpoints',
'prerequisites': [],
'alternatives': ['kpoints-kpoints']
},
'occupancies': {
'inputs': [],
'name': 'occupancies',
'prerequisites': [],
},
'trajectory': {
'inputs': [],
'name': 'trajectory',
'prerequisites': [],
},
'energies': {
'inputs': [],
'name': 'energies',
'prerequisites': [],
},
'total_energies': {
'inputs': [],
'name': 'total_energies',
'prerequisites': [],
},
'projectors': {
'inputs': [],
'name': 'projectors',
'prerequisites': [],
},
'dielectrics': {
'inputs': [],
'name': 'dielectrics',
'prerequisites': [],
},
'stress': {
'inputs': [],
'name': 'stress',
'prerequisites': [],
},
'forces': {
'inputs': [],
'name': 'forces',
'prerequisites': [],
},
'born_charges': {
'inputs': [],
'name': 'born_charges',
'prerequisites': [],
},
'hessian': {
'inputs': [],
'name': 'hessian',
'prerequisites': [],
},
'dynmat': {
'inputs': [],
'name': 'dynmat',
'prerequisites': [],
},
'fermi_level': {
'inputs': [],
'name': 'fermi_level',
'prerequisites': [],
},
'maximum_force': {
'inputs': [],
'name': 'maximum_force',
'prerequisites': []
},
'maximum_stress': {
'inputs': [],
'name': 'maximum_stress',
'prerequisites': []
},
'band_properties': {
'inputs': [],
'name': 'band_properties',
'prerequisites': [],
},
'version': {
'inputs': [],
'name': 'version',
'prerequisites': [],
}
}
# Mapping of the energy names to those returned by parsevasp.vasprunl.Xml
ENERGY_MAPPING = {
'energy_extrapolated': 'energy_extrapolated_final',
'energy_free': 'energy_free_final',
'energy_no_entropy': 'energy_no_entropy_final',
'energy_extrapolated_electronic': 'energy_extrapolated',
'energy_free_electronic': 'energy_free',
'energy_no_entropy_electronic': 'energy_no_entropy',
}
ENERGY_MAPPING_VASP5 = {
'energy_extrapolated': 'energy_no_entropy_final',
'energy_free': 'energy_free_final',
# Not that energy_extrapolated_final parsed is the entropy term
'energy_no_entropy': 'energy_extrapolated_final',
'energy_extrapolated_electronic': 'energy_extrapolated',
'energy_free_electronic': 'energy_free',
'energy_no_entropy_electronic': 'energy_no_entropy',
}
def _init_from_handler(self, handler):
"""Initialize using a file like handler."""
self.overflow = False
try:
self._content_parser = Xml(file_handler=handler, k_before_band=True, logger=self._logger)
except SystemExit as exception:
if exception.code == 509:
# Xml might be fine but overflow is detected
self.overflow = True
self._logger.warning('Parsevasp exited abnormally due to overflow in XML file.')
else:
self._logger.warning('Parsevasp exited abnormally.')
@property
def version(self):
"""Fetch the VASP version from ``parsevasp`` and return it as a string object."""
# fetch version
version = self._content_parser.get_version()
if version is None:
return None
return version
@property
def eigenvalues(self):
"""Fetch eigenvalues."""
# Fetch eigenvalues
eigenvalues = self._content_parser.get_eigenvalues()
if eigenvalues is None:
return None
return eigenvalues
@property
def occupancies(self):
"""Fetch occupancies."""
# Fetch occupancies
occupancies = self._content_parser.get_occupancies()
if occupancies is None:
# occupancies not present, should not really happen?
return None
return occupancies
@property
def kpoints(self):
"""Fetch the kpoints an prepare for consumption by the NodeComposer."""
kpts = self._content_parser.get_kpoints()
kptsw = self._content_parser.get_kpointsw()
# k-points in XML is always in reciprocal if spacing methods have been used
# but what about explicit/regular
cartesian = False
kpoints_data = None
if (kpts is not None) and (kptsw is not None):
# Create a dictionary and store k-points that can be consumed by the NodeComposer
kpoints_data = {}
kpoints_data['mode'] = 'explicit'
kpoints_data['cartesian'] = cartesian
kpoints_data['points'] = kpts
kpoints_data['weights'] = kptsw
return kpoints_data
@property
def structure(self):
"""
Fetch a given structure.
Which structure to fetch is controlled by inputs.
eFL: Need to clean this so that we can set different
structures to pull from the outside. Could be usefull not
pulling the whole trajectory.
Currently defaults to the last structure.
"""
return self.last_structure
@property
def last_structure(self):
"""
Fetch the structure.
After or at the last recorded ionic step.
"""
last_lattice = self._content_parser.get_lattice('last')
if last_lattice is None:
return None
return _build_structure(last_lattice)
@property
def final_structure(self):
"""
Fetch the structure.
After or at the last recorded ionic step. Should in
principle be the same as the method above.
"""
return self.last_structure
@property
def last_forces(self):
"""
Fetch forces.
After or at the last recorded ionic step.
"""
force = self._content_parser.get_forces('last')
return force
@property
def final_forces(self):
"""
Fetch forces.
After or at the last recorded ionic step.
"""
return self.last_forces
@property
def forces(self):
"""
Fetch forces.
This container should contain all relevant forces.
Currently, it only contains the final forces, which can be obtain
by the id `final_forces`.
"""
final_forces = self.final_forces
forces = {'final': final_forces}
return forces
@property
def maximum_force(self):
"""Fetch the maximum force of at the last ionic run."""
forces = self.final_forces
if forces is None:
return None
norm = np.linalg.norm(forces, axis=1)
return np.amax(np.abs(norm))
@property
def last_stress(self):
"""
Fetch stess.
After or at the last recorded ionic step.
"""
stress = self._content_parser.get_stress('last')
return stress
@property
def final_stress(self):
"""
Fetch stress.
After or at the last recorded ionic step.
"""
return self.last_stress
@property
def stress(self):
"""
Fetch stress.
This container should contain all relevant stress.
Currently, it only contains the final stress, which can be obtain
by the id `final_stress`.
"""
final_stress = self.final_stress
stress = {'final': final_stress}
return stress
@property
def maximum_stress(self):
"""Fetch the maximum stress of at the last ionic run."""
stress = self.final_stress
if stress is None:
return None
norm = np.linalg.norm(stress, axis=1)
return np.amax(np.abs(norm))
@property
def trajectory(self):
"""
Fetch unitcells, positions, species, forces and stress.
For all calculation steps.
"""
unitcell = self._content_parser.get_unitcell('all')
positions = self._content_parser.get_positions('all')
species = self._content_parser.get_species()
forces = self._content_parser.get_forces('all')
stress = self._content_parser.get_stress('all')
# make sure all are sorted, first to last calculation
# (species is constant)
unitcell = sorted(unitcell.items())
positions = sorted(positions.items())
forces = sorted(forces.items())
stress = sorted(stress.items())
# convert to numpy
unitcell = np.asarray([item[1] for item in unitcell])
positions = np.asarray([item[1] for item in positions])
forces = np.asarray([item[1] for item in forces])
stress = np.asarray([item[1] for item in stress])
# Aiida wants the species as symbols, so invert
elements = _invert_dict(parsevaspct.elements)
symbols = np.asarray([elements[item].title() for item in species.tolist()])
if (unitcell is not None) and (positions is not None) and \
(species is not None) and (forces is not None) and \
(stress is not None):
trajectory_data = {}
keys = ('cells', 'positions', 'symbols', 'forces', 'stress', 'steps')
stepids = np.arange(unitcell.shape[0])
for key, data in zip(keys, (unitcell, positions, symbols, forces, stress, stepids)):
trajectory_data[key] = data
return trajectory_data
return None
@property
def total_energies(self):
"""Fetch the total energies after the last ionic run."""
energies = self.energies
if energies is None:
return None
energies_dict = {}
for etype in self._settings.get('energy_type', self.DEFAULT_SETTINGS['energy_type']):
energies_dict[etype] = energies[etype][-1]
# Also return the raw electronic steps energy
energies_dict[etype + '_electronic'] = energies[etype + '_electronic'][-1]
return energies_dict
@property
def energies(self):
"""Fetch the total energies."""
# Check if we want total energy entries for each electronic step.
electronic_step_energies = self._settings.get(
'electronic_step_energies', self.DEFAULT_SETTINGS['electronic_step_energies']
)
return self._energies(nosc=not electronic_step_energies)
def _energies(self, nosc):
"""
Fetch the total energies for all energy types, calculations (ionic steps) and electronic steps.
The returned dict from the parser contains the total energy types as a key (plus the _final, which is
the final total energy ejected by VASP after the closure of the electronic steps). The energies can then
be found in the flattened ndarray where the key `electronic_steps` indicate how many electronic steps
there is per ionic step. Using the combination, one can rebuild the electronic step energy per ionic step etc.
Because the VASPrun parser returns both the electronic step energies (at the end of each cycles) and the ionic step
energies (_final), we apply a mapping to recovery the naming such that the ionic step energies do not have the suffix,
but the electronic step energies do.
"""
etype = self._settings.get('energy_type', self.DEFAULT_SETTINGS['energy_type'])
# Create a copy
etype = list(etype)
etype_orig = list(etype)
# Apply mapping and request the correct energies from the parsing results
# VASP 5 has a bug where the energy_no_entropy is not included in the XML output - we have to calculate it here
if self.version.startswith('5'):
# For energy_no_entropy needs to be calculated here
if 'energy_no_entropy' in etype_orig:
etype.append('energy_free')
etype.append('energy_extrapolated')
# energy extrapolated is stored as energy_no_entropy for the ionic steps
if 'energy_extrapolated' in etype_orig:
etype.append('energy_no_entropy')
# Remove duplicates
etype = list(set(etype))
energies = self._content_parser.get_energies(status='all', etype=etype, nosc=nosc)
# Here we must calculate the true `energy_no_entropy`
if 'energy_no_entropy' in etype_orig:
# The energy_extrapolated_final is the entropy term itself in VASP 5
# Store the calculated energy_no_entropy under 'energy_extrapolated_final',
# which is then recovered as `energy_no_entropy` later
energies['energy_extrapolated_final'
] = energies['energy_free_final'] - energies['energy_extrapolated_final']
else:
energies = self._content_parser.get_energies(status='all', etype=etype, nosc=nosc)
if energies is None:
return None
# Apply mapping - those with `_final` has the suffix removed and those without has `_electronic` added
mapped_energies = {}
mapping = self.ENERGY_MAPPING_VASP5 if self.version.startswith('5') else self.ENERGY_MAPPING
# Reverse the mapping - now key is the name of the original energies output
revmapping = {value: key for key, value in mapping.items()}
for key, value in energies.items():
# Apply mapping if needed
if key in revmapping:
if revmapping[key].replace('_electronic', '') in etype_orig:
mapped_energies[revmapping[key]] = value
else:
mapped_energies[key] = value
return mapped_energies
@property
def projectors(self):
"""Fetch the projectors."""
proj = self._content_parser.get_projectors()
if proj is None:
return None
projectors = {}
prj = []
try:
prj.append(proj['total']) # pylint: disable=unsubscriptable-object
except KeyError:
try:
prj.append(proj['up']) # pylint: disable=unsubscriptable-object
prj.append(proj['down']) # pylint: disable=unsubscriptable-object
except KeyError:
self._logger.error('Did not detect any projectors. Returning.')
if len(prj) == 1:
projectors['projectors'] = prj[0]
else:
projectors['projectors'] = np.asarray(prj)
return projectors
@property
def dielectrics(self):
"""Fetch the dielectric function."""
diel = self._content_parser.get_dielectrics()
if diel is None:
return None
dielectrics = {}
energy = diel.get('energy')
idiel = diel.get('imag')
rdiel = diel.get('real')
epsilon = diel.get('epsilon')
epsilon_ion = diel.get('epsilon_ion')
if energy is not None:
dielectrics['ediel'] = energy
if idiel is not None:
dielectrics['rdiel'] = rdiel
if rdiel is not None:
dielectrics['idiel'] = idiel
if epsilon is not None:
dielectrics['epsilon'] = epsilon
if epsilon_ion is not None:
dielectrics['epsilon_ion'] = epsilon_ion
return dielectrics
@property
def born_charges(self):
"""Fetch the Born effective charges."""
brn = self._content_parser.get_born()
if brn is None:
return None
born = {'born_charges': brn}
return born
@property
def hessian(self):
"""Fetch the Hessian matrix."""
hessian = self._content_parser.get_hessian()
if hessian is None:
return None
hess = {'hessian': hessian}
return hess
@property
def dynmat(self):
"""Fetch the dynamical eigenvectors and eigenvalues."""
dynmat = self._content_parser.get_dynmat()
if dynmat is None:
return None
dyn = {}
dyn['dynvec'] = dynmat['eigenvectors'] # pylint: disable=unsubscriptable-object
dyn['dyneig'] = dynmat['eigenvalues'] # pylint: disable=unsubscriptable-object
return dyn
@property
def dos(self):
"""Fetch the total density of states."""
dos = self._content_parser.get_dos()
if dos is None:
return None
densta = {}
# energy is always there, regardless of
# total, spin or partial
energy = dos['total']['energy'] # pylint: disable=unsubscriptable-object
densta['energy'] = energy
tdos = None
pdos = None
upspin = dos.get('up')
downspin = dos.get('down')
total = dos.get('total')
if (upspin is not None) and (downspin is not None):
tdos = np.stack((upspin['total'], downspin['total']))
if (upspin['partial'] is not None) and \
(downspin['partial'] is not None):
pdos = np.stack((upspin['partial'], downspin['partial']))
else:
tdos = total['total']
pdos = total['partial']
densta['tdos'] = tdos
if pdos is not None:
densta['pdos'] = pdos
return densta
@property
def fermi_level(self):
"""Fetch Fermi level."""
return self._content_parser.get_fermi_level()
@property
def run_status(self):
"""Fetch run_status information"""
info = {}
# First check electronic convergence by comparing executed steps to the
# maximum allowed number of steps (NELM).
energies = self._content_parser.get_energies('last', nosc=False)
parameters = self._content_parser.get_parameters()
info['finished'] = not self._content_parser.truncated
# Only set to true for untruncated run to avoid false positives
if energies is None:
info['electronic_converged'] = False
elif energies.get('electronic_steps')[0] < parameters['nelm'] and not self._content_parser.truncated:
info['electronic_converged'] = True
else:
info['electronic_converged'] = False
# Then check the ionic convergence by comparing executed steps to the
# maximum allowed number of steps (NSW).
energies = self._content_parser.get_energies('all', nosc=True)
if energies is None:
info['ionic_converged'] = False
else:
if len(energies.get('electronic_steps')) < parameters['nsw'] and not self._content_parser.truncated:
info['ionic_converged'] = True
else:
info['ionic_converged'] = False
# Override if nsw is 0 - no ionic steps are performed
if parameters['nsw'] < 1:
info['ionic_converged'] = None
return info
@property
def band_properties(self):
"""Fetch key properties of the electronic structure."""
eigenvalues = self.eigenvalues
occupancies = self.occupancies
if eigenvalues is None:
return None
# Convert dict to index in numpy array
if 'total' in eigenvalues:
eig = np.array(eigenvalues['total'])
occ = np.array(occupancies['total'])
else:
eig = np.array([eigenvalues['up'], eigenvalues['down']])
occ = np.array([occupancies['up'], occupancies['down']])
return get_band_properties(eig, occ)
def _build_structure(lattice):
"""Builds a structure according to AiiDA spec."""
structure_dict = {}
structure_dict['unitcell'] = lattice['unitcell']
structure_dict['sites'] = []
# AiiDA wants the species as symbols, so invert
elements = _invert_dict(parsevaspct.elements)
for pos, specie in zip(lattice['positions'], lattice['species']):
site = {}
site['position'] = np.dot(pos, lattice['unitcell'])
site['symbol'] = elements[specie].title()
site['kind_name'] = elements[specie].title()
structure_dict['sites'].append(site)
return structure_dict
def _invert_dict(dct):
return dct.__class__(map(reversed, dct.items()))