dft_tools/python/triqs_dft_tools/converters/plovasp/inpconf.py

662 lines
23 KiB
Python

################################################################################
#
# TRIQS: a Toolbox for Research in Interacting Quantum Systems
#
# Copyright (C) 2011 by M. Ferrero, O. Parcollet
#
# DFT tools: Copyright (C) 2011 by M. Aichhorn, L. Pourovskii, V. Vildosola
#
# PLOVasp: Copyright (C) 2015 by O. E. Peil
#
# TRIQS 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.
#
# TRIQS 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
# TRIQS. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
r"""
plovasp.inpconfig
=================
Module for parsing and checking an input config-file.
"""
import configparser
import numpy as np
import re
import sys
import itertools as it
from . import vaspio
def issue_warning(message):
"""
Issues a warning.
"""
print()
print(" !!! WARNING !!!: " + message)
print()
################################################################################
################################################################################
#
# class ConfigParameters
#
################################################################################
################################################################################
class ConfigParameters:
r"""
Class responsible for parsing of the input config-file.
Parameters:
- *sh_required*, *sh_optional* : required and optional parameters of shells
- *gr_required*, *gr_optional* : required and optional parameters of groups
The dictionary contains a mapping of conf-file keywords to
a pair of objects:
1. internal name of a parameter
2. function used to convert an input string into data for a given parameter
"""
################################################################################
#
# __init__()
#
################################################################################
def __init__(self, input_filename, verbosity=1):
self.verbosity = verbosity
self.cp = configparser.SafeConfigParser()
self.cp.readfp(open(input_filename, 'r'))
self.parameters = {}
self.sh_required = {
'ions': ('ions', self.parse_string_ion_list),
'lshell': ('lshell', int)}
self.sh_optional = {
'transform': ('tmatrix', lambda s: self.parse_string_tmatrix(s, real=True)),
'transfile': ('tmatrices', self.parse_file_tmatrix),
'sort': ('ion_sort', self.parse_string_int,None),
'corr': ('corr', self.parse_string_logical, True)}
self.gr_required = {
'shells': ('shells', lambda s: list(map(int, s.split()))),
'ewindow': ('ewindow', self.parse_energy_window)}
self.gr_optional = {
'normalize' : ('normalize', self.parse_string_logical, True),
'normion' : ('normion', self.parse_string_logical, True),
'complement' : ('complement', self.parse_string_logical, False),
'bands': ('bands', self.parse_band_window)}
self.gen_optional = {
'basename' : ('basename', str, 'vasp'),
'efermi' : ('efermi', float),
'dosmesh': ('dosmesh', self.parse_string_dosmesh),
'hk': ('hk', self.parse_string_logical, False)}
#
# Special parsers
#
################################################################################
#
# parse_string_ion_list()
#
################################################################################
def parse_string_ion_list(self, par_str):
"""
The ion list accepts the following formats:
1). A list of ion indices according to POSCAR.
The list can be defined as a range '9..20'.
2). A list of ion groups (e.g. '[1 4] [2 3]') in which
case each group defines a set of equivalent sites.
3). An element name, in which case all ions with
this name are included. NOT YET IMPLEMENTED.
The second option requires an input from POSCAR file.
"""
ion_info = {}
# First check if a range is given
patt = '([0-9]+)\.\.([0-9]+)'
match = re.match(patt, par_str)
if match:
i1, i2 = tuple(map(int, match.groups()[:2]))
mess = "First index of the range must be smaller or equal to the second"
assert i1 <= i2, mess
# Note that we need to subtract 1 from VASP indices
ion_info['ion_list'] = [[ion - 1] for ion in range(i1, i2 + 1)]
ion_info['nion'] = len(ion_info['ion_list'])
else:
# Check if a set of indices is given
try:
l_tmp = list(map(int, par_str.split()))
l_tmp.sort()
# Subtract 1 so that VASP indices (starting with 1) are converted
# to Python indices (starting with 0)
ion_info['ion_list'] = [[ion - 1] for ion in l_tmp]
ion_info['nion'] = len(ion_info['ion_list'])
except ValueError:
pass
# Check if equivalence classes are given
if not ion_info:
try:
patt = '[0-9][0-9,\s]*'
patt2 = '[0-9]+'
classes = re.findall(patt, par_str)
ion_list = []
nion = 0
for cl in classes:
ions = list(map(int, re.findall(patt2, cl)))
ion_list.append([ion - 1 for ion in ions])
nion += len(ions)
if not ion_list:
raise ValueError
ion_info['ion_list'] = ion_list
ion_info['nion'] = nion
except ValueError:
err_msg = "Error parsing list of ions"
raise NotImplementedError(err_msg)
if 'ion_list' in ion_info:
ion_list = ion_info['ion_list']
assert all([all([ion >= 0 for ion in gr]) for gr in ion_list]), (
"Lowest ion index is smaller than 1 in '%s'"%(par_str))
return ion_info
################################################################################
#
# parse_string_logical()
#
################################################################################
def parse_string_logical(self, par_str):
"""
Logical parameters are given by string 'True' or 'False'
(case does not matter). In fact, only the first symbol matters so that
one can write 'T' or 'F'.
"""
first_char = par_str[0].lower()
assert first_char in 'tf', "Logical parameters should be given by either 'True' or 'False'"
return first_char == 't'
################################################################################
#
# parse_string_int()
#
################################################################################
def parse_string_int(self, par_str):
"""
int parameters
"""
return int(par_str)
################################################################################
#
# parse_energy_window()
#
################################################################################
def parse_energy_window(self, par_str):
"""
Energy window is given by two floats, with the first one being smaller
than the second one.
"""
ftmp = list(map(float, par_str.split()))
assert len(ftmp) == 2, "EWINDOW must be specified by exactly two floats"
assert ftmp[0] < ftmp[1], "The first float in EWINDOW must be smaller than the second one"
return tuple(ftmp)
################################################################################
#
# parse_band_window()
#
################################################################################
def parse_band_window(self, par_str):
"""
Band window is given by two ints, with the first one being smaller
than the second one.
"""
ftmp = list(map(int, par_str.split()))
assert len(ftmp) == 2, "BANDS must be specified by exactly two ints"
assert ftmp[0] < ftmp[1], "The first int in BANDS must be smaller than the second one"
return tuple(ftmp)
################################################################################
#
# parse_string_tmatrix()
#
################################################################################
def parse_string_tmatrix(self, par_str, real):
"""
Transformation matrix is defined as a set of rows separated
by a new line symbol.
"""
str_rows = par_str.split('\n')
try:
rows = [list(map(float, s.split())) for s in str_rows]
except ValueError:
err_mess = "Cannot parse a matrix string:\n%s"%(par_str)
raise ValueError(err_mess)
nr = len(rows)
nm = len(rows[0])
err_mess = "Number of columns must be the same:\n%s"%(par_str)
for row in rows:
assert len(row) == nm, err_mess
if real:
mat = np.array(rows)
else:
err_mess = "Complex matrix must contain 2*M values:\n%s"%(par_str)
assert 2 * (nm // 2) == nm, err_mess
tmp = np.array(rows, dtype=complex)
mat = tmp[:, 0::2] + 1.0j * tmp[:, 1::2]
return mat
################################################################################
#
# parse_file_tmatrix()
#
################################################################################
def parse_file_tmatrix(self, filename):
"""
Parses a file 'filename' containing transformation matrices
for each ion. The parser returns a raw matrix that will be
interpreted elsewhere because the interpretation depends on
shell parameters.
"""
tmatrices = np.loadtxt(filename)
return tmatrices
################################################################################
#
# parse_string_dosmesh()
#
################################################################################
def parse_string_dosmesh(self, par_str):
"""
Two formats are accepted:
1. Two floats (energy range) and an integer (number of energy points).
2. One integer (number of energy points). In this case the energy
range is taken to be equal to EMIN, EMAX of a shell.
The parser returns a dictionary:
{'n_points': int,
'emin': float,
'emax': float}
If the second option is used, 'emin' and 'emax' are undefined
and set to 'nan'.
"""
stmp = par_str.split()
if len(stmp) == 3:
emin, emax = float(stmp[0]), float(stmp[1])
n_points = int(stmp[2])
elif len(stmp) == 1:
n_points = int(stmp[0])
emin = emax = float('nan')
else:
err_mess = "DOSMESH must be either 'EMIN EMAX NPOINTS' or 'NPOINTS'"
raise ValueError(err_mess)
dos_pars = {
'n_points': n_points,
'emin': emin,
'emax': emax}
return dos_pars
################################################################################
#
# parse_parameter_set()
#
################################################################################
def parse_parameter_set(self, section, param_set, exception=False, defaults=True):
"""
Parses required or optional parameter set from a section.
For required parameters `exception=True` must be set.
"""
parsed = {}
for par in list(param_set.keys()):
key = param_set[par][0]
try:
par_str = self.cp.get(section, par)
except (configparser.NoOptionError, configparser.NoSectionError):
if exception:
message = "Required parameter '%s' not found in section [%s]"%(par, section)
raise Exception(message)
else:
# Use the default value if there is one
if defaults and len(param_set[par]) > 2:
parsed[key] = param_set[par][2]
continue
if self.verbosity > 0:
print(" %s = %s"%(par, par_str))
parse_fun = param_set[par][1]
parsed[key] = parse_fun(par_str)
return parsed
################################################################################
#
# parse_shells()
#
################################################################################
def parse_shells(self):
"""
Parses all [Shell] sections.
"""
# Find all [Shell] sections
# (note that ConfigParser transforms all names to lower case)
sections = self.cp.sections()
sh_patt1 = re.compile('shell +.*', re.IGNORECASE)
sec_shells = list(filter(sh_patt1.match, sections))
self.nshells = len(sec_shells)
assert self.nshells > 0, "No projected shells found in the input file"
if self.verbosity > 0:
print()
if self.nshells > 1:
print(" Found %i projected shells"%(self.nshells))
else:
print(" Found 1 projected shell")
# Get shell indices
sh_patt2 = re.compile('shell +([0-9]*)$', re.IGNORECASE)
try:
get_ind = lambda s: int(sh_patt2.match(s).groups()[0])
sh_inds = list(map(get_ind, sec_shells))
except (ValueError, AttributeError):
raise ValueError("Failed to extract shell indices from a list: %s"%(sec_shells))
self.sh_sections = {ind: sec for ind, sec in zip(sh_inds, sec_shells)}
# Check that all indices are unique
# In principle redundant because the list of sections will contain only unique names
assert len(sh_inds) == len(set(sh_inds)), "There must be no shell with the same index!"
# Ideally, indices should run from 1 to <nshells>
# If it's not the case, issue a warning
sh_inds.sort()
if sh_inds != list(range(1, len(sh_inds) + 1)):
issue_warning("Shell indices are not uniform or not starting from 1. "
"This might be an indication of a incorrect setup.")
# Parse shell parameters and put them into a list sorted according to the original indices
self.shells = []
for ind in sh_inds:
shell = {}
# Store the original user-defined index
shell['user_index'] = ind
section = self.sh_sections[ind]
if self.verbosity > 0:
print()
print(" Shell parameters:")
# Shell required parameters
parsed = self.parse_parameter_set(section, self.sh_required, exception=True)
shell.update(parsed)
# Shell optional parameters
parsed = self.parse_parameter_set(section, self.sh_optional, exception=False)
shell.update(parsed)
# Group required parameters
# Must be given if no group is explicitly specified
# If in conflict with the [Group] section, the latter has a priority
parsed = self.parse_parameter_set(section, self.gr_required, exception=False)
shell.update(parsed)
# Group optional parameters
parsed = self.parse_parameter_set(section, self.gr_optional, exception=False, defaults=False)
shell.update(parsed)
self.shells.append(shell)
################################################################################
#
# parse_groups()
#
################################################################################
def parse_groups(self):
"""
Parses [Group] sections.
"""
# Find group sections
sections = self.cp.sections()
gr_patt = re.compile('group +(.*)', re.IGNORECASE)
sec_groups = list(filter(gr_patt.match, sections))
self.ngroups = len(sec_groups)
self.groups = []
# Parse group parameters
for section in sec_groups:
group = {}
# Extract group index (FIXME: do we really need it?)
gr_patt2 = re.compile('group +([0-9]*)$', re.IGNORECASE)
try:
gr_ind = int(gr_patt2.match(section).groups()[0])
except (ValueError, AttributeError):
raise ValueError("Failed to extract group index from a group name: %s"%(section))
group['index'] = gr_ind
if self.verbosity > 0:
print()
print(" Group parameters:")
# Group required parameters
parsed = self.parse_parameter_set(section, self.gr_required, exception=True)
group.update(parsed)
# Group optional parameters
parsed = self.parse_parameter_set(section, self.gr_optional, exception=False)
group.update(parsed)
self.groups.append(group)
# Sort groups according to indices defined in the config-file
if self.ngroups > 0:
self.groups.sort(key=lambda g: g['index'])
################################################################################
#
# groups_shells_consistency()
#
################################################################################
def groups_shells_consistency(self):
"""
Ensures consistency between groups and shells. In particular:
- if no groups are explicitly defined and only shell is defined create a group automatically
- check the existance of all shells referenced in the groups
- check that all shells are referenced in the groups
"""
# Special case: no groups is defined
if self.ngroups == 0:
# Check that 'nshells = 1'
assert self.nshells == 1, "At least one group must be defined if there are more than one shells."
# Otherwise create a single group taking group information from [Shell] section
self.groups.append({})
self.groups[0]['index'] = '1'
# Check that the single '[Shell]' section contains enough information
# (required group parameters except 'shells')
# and move it to the `groups` dictionary
sh_gr_required = dict(self.gr_required)
sh_gr_required.pop('shells')
try:
for par in list(sh_gr_required.keys()):
key = sh_gr_required[par][0]
value = self.shells[0].pop(key)
self.groups[0][key] = value
except KeyError:
message = "One [Shell] section is specified but no explicit [Group] section is provided."
message += " In this case the [Shell] section must contain all required group information.\n"
message += " Required parameters are: %s"%(list(sh_gr_required.keys()))
raise KeyError(message)
# Do the same for optional group parameters, but do not raise an exception
for par in list(self.gr_optional.keys()):
try:
key = self.gr_optional[par][0]
value = self.shells[0].pop(key)
self.groups[0][key] = value
except KeyError:
if len(self.gr_optional[par]) > 2:
self.groups[0][key] = self.gr_optional[par][2]
continue
# Add the index of the single shell into the group
self.groups[0].update({'shells': [1]})
#
# Consistency checks
#
# Check the existence of shells referenced in the groups
def find_shell_by_user_index(uindex):
for ind, shell in enumerate(self.shells):
if shell['user_index'] == uindex:
return ind, shell
raise KeyError
sh_all_inds = []
for group in self.groups:
gr_shells = group['shells']
sh_inds = []
for user_ind in gr_shells:
try:
ind, shell = find_shell_by_user_index(user_ind)
except KeyError:
raise Exception("Shell %i referenced in group '%s' does not exist"%(user_ind, group['index']))
sh_inds.append(ind)
# If [Shell] section contains (potentially conflicting) group parameters
# remove them and issue a warning.
#
# First, required group parameters
for par in list(self.gr_required.keys()):
try:
key = self.gr_required[par][0]
value = shell.pop(key)
mess = (" Redundant group parameter '%s' in [Shell] section"
" %i is discarded"%(par, user_ind))
issue_warning(mess)
except KeyError:
continue
# Second, optional group parameters
for par in list(self.gr_optional.keys()):
try:
key = self.gr_optional[par][0]
value = shell.pop(key)
mess = (" Redundant group parameter '%s' in [Shell] section"
" %i is discarded"%(par, user_ind))
issue_warning(mess)
except KeyError:
continue
sh_all_inds += sh_inds
# Replace user shell indices with internal ones
group['shells'] = sh_inds
sh_refs_used = list(set(sh_all_inds))
sh_refs_used.sort()
# Check that all shells are referenced in the groups
assert sh_refs_used == list(range(self.nshells)), "Some shells are not inside any of the groups"
################################################################################
#
# parse_general()
#
################################################################################
def parse_general(self):
"""
Parses [General] section.
"""
self.general = {}
sections = self.cp.sections()
gen_section = [s for s in sections if s.lower() == 'general']
# If no [General] section is found parse a dummy section name to the parser
# to reset parameters to their default values
if len(gen_section) > 1:
raise Exception("More than one section [General] is found")
if len(gen_section) == 0:
gen_section = 'general'
gen_section = gen_section[0]
parsed = self.parse_parameter_set(gen_section, self.gen_optional, exception=False)
self.general.update(parsed)
################################################################################
#
# Main parser function
#
################################################################################
def parse_input(self):
"""
Parses input conf-file.
"""
self.parse_general()
self.parse_shells()
self.parse_groups()
self.groups_shells_consistency()
#
# Obsolete part
#
if __name__ == '__main__':
narg = len(sys.argv)
if narg < 2:
raise SystemExit(" Usage: python pyconf.py <conf-file> [<path-to-vasp-calcultaion>]")
else:
filename = sys.argv[1]
if narg > 2:
vasp_dir = sys.argv[2]
if vasp_dir[-1] != '/':
vasp_dir += '/'
else:
vasp_dir = './'
# plocar = vaspio.Plocar()
# plocar.from_file(vasp_dir)
# poscar = vaspio.Poscar()
# poscar.from_file(vasp_dir)
# kpoints = vaspio.Kpoints()
# kpoints.from_file(vasp_dir)
eigenval = vaspio.Eigenval()
eigenval.from_file(vasp_dir)
doscar = vaspio.Doscar()
doscar.from_file(vasp_dir)
# pars = parse_input(filename)