Source code for ase.io.abinit

import os
from os.path import join
import re
from glob import glob
import warnings

import numpy as np

from ase import Atoms
from ase.data import chemical_symbols
from ase.units import Hartree, Bohr, fs
from ase.calculators.calculator import Parameters


[docs]def read_abinit_in(fd): """Import ABINIT input file. Reads cell, atom positions, etc. from abinit input file """ tokens = [] for line in fd: meat = line.split('#', 1)[0] tokens += meat.lower().split() # note that the file can not be scanned sequentially index = tokens.index("acell") unit = 1.0 if(tokens[index + 4].lower()[:3] != 'ang'): unit = Bohr acell = [unit * float(tokens[index + 1]), unit * float(tokens[index + 2]), unit * float(tokens[index + 3])] index = tokens.index("natom") natom = int(tokens[index + 1]) index = tokens.index("ntypat") ntypat = int(tokens[index + 1]) index = tokens.index("typat") typat = [] while len(typat) < natom: token = tokens[index + 1] if '*' in token: # e.g. typat 4*1 3*2 ... nrepeat, typenum = token.split('*') typat += [int(typenum)] * int(nrepeat) else: typat.append(int(token)) index += 1 assert natom == len(typat) index = tokens.index("znucl") znucl = [] for i in range(ntypat): znucl.append(int(tokens[index + 1 + i])) index = tokens.index("rprim") rprim = [] for i in range(3): rprim.append([acell[i] * float(tokens[index + 3 * i + 1]), acell[i] * float(tokens[index + 3 * i + 2]), acell[i] * float(tokens[index + 3 * i + 3])]) # create a list with the atomic numbers numbers = [] for i in range(natom): ii = typat[i] - 1 numbers.append(znucl[ii]) # now the positions of the atoms if "xred" in tokens: index = tokens.index("xred") xred = [] for i in range(natom): xred.append([float(tokens[index + 3 * i + 1]), float(tokens[index + 3 * i + 2]), float(tokens[index + 3 * i + 3])]) atoms = Atoms(cell=rprim, scaled_positions=xred, numbers=numbers, pbc=True) else: if "xcart" in tokens: index = tokens.index("xcart") unit = Bohr elif "xangst" in tokens: unit = 1.0 index = tokens.index("xangst") else: raise IOError( "No xred, xcart, or xangs keyword in abinit input file") xangs = [] for i in range(natom): xangs.append([unit * float(tokens[index + 3 * i + 1]), unit * float(tokens[index + 3 * i + 2]), unit * float(tokens[index + 3 * i + 3])]) atoms = Atoms(cell=rprim, positions=xangs, numbers=numbers, pbc=True) try: ii = tokens.index('nsppol') except ValueError: nsppol = None else: nsppol = int(tokens[ii + 1]) if nsppol == 2: index = tokens.index('spinat') magmoms = [float(tokens[index + 3 * i + 3]) for i in range(natom)] atoms.set_initial_magnetic_moments(magmoms) assert len(atoms) == natom return atoms
keys_with_units = { 'toldfe': 'eV', 'tsmear': 'eV', 'paoenergyshift': 'eV', 'zmunitslength': 'Bohr', 'zmunitsangle': 'rad', 'zmforcetollength': 'eV/Ang', 'zmforcetolangle': 'eV/rad', 'zmmaxdispllength': 'Ang', 'zmmaxdisplangle': 'rad', 'ecut': 'eV', 'pawecutdg': 'eV', 'dmenergytolerance': 'eV', 'electronictemperature': 'eV', 'oneta': 'eV', 'onetaalpha': 'eV', 'onetabeta': 'eV', 'onrclwf': 'Ang', 'onchemicalpotentialrc': 'Ang', 'onchemicalpotentialtemperature': 'eV', 'mdmaxcgdispl': 'Ang', 'mdmaxforcetol': 'eV/Ang', 'mdmaxstresstol': 'eV/Ang**3', 'mdlengthtimestep': 'fs', 'mdinitialtemperature': 'eV', 'mdtargettemperature': 'eV', 'mdtargetpressure': 'eV/Ang**3', 'mdnosemass': 'eV*fs**2', 'mdparrinellorahmanmass': 'eV*fs**2', 'mdtaurelax': 'fs', 'mdbulkmodulus': 'eV/Ang**3', 'mdfcdispl': 'Ang', 'warningminimumatomicdistance': 'Ang', 'rcspatial': 'Ang', 'kgridcutoff': 'Ang', 'latticeconstant': 'Ang'}
[docs]def write_abinit_in(fd, atoms, param=None, species=None, pseudos=None): import copy from ase.calculators.calculator import kpts2mp from ase.calculators.abinit import Abinit if param is None: param = {} _param = copy.deepcopy(Abinit.default_parameters) _param.update(param) param = _param if species is None: species = sorted(set(atoms.numbers)) inp = {} inp.update(param) for key in ['xc', 'smearing', 'kpts', 'pps', 'raw']: del inp[key] smearing = param.get('smearing') if 'tsmear' in param or 'occopt' in param: assert smearing is None if smearing is not None: inp['occopt'] = {'fermi-dirac': 3, 'gaussian': 7}[smearing[0].lower()] inp['tsmear'] = smearing[1] inp['natom'] = len(atoms) if 'nbands' in param: inp['nband'] = param['nbands'] del inp['nbands'] # ixc is set from paw/xml file. Ignore 'xc' setting then. if param.get('pps') not in ['pawxml']: if 'ixc' not in param: inp['ixc'] = {'LDA': 7, 'PBE': 11, 'revPBE': 14, 'RPBE': 15, 'WC': 23}[param['xc']] magmoms = atoms.get_initial_magnetic_moments() if magmoms.any(): inp['nsppol'] = 2 fd.write('spinat\n') for n, M in enumerate(magmoms): fd.write('%.14f %.14f %.14f\n' % (0, 0, M)) else: inp['nsppol'] = 1 if param['kpts'] is not None: mp = kpts2mp(atoms, param['kpts']) fd.write('kptopt 1\n') fd.write('ngkpt %d %d %d\n' % tuple(mp)) fd.write('nshiftk 1\n') fd.write('shiftk\n') fd.write('%.1f %.1f %.1f\n' % tuple((np.array(mp) + 1) % 2 * 0.5)) valid_lists = (list, np.ndarray) for key in sorted(inp): value = inp[key] unit = keys_with_units.get(key) if unit is not None: if 'fs**2' in unit: value /= fs**2 elif 'fs' in unit: value /= fs if isinstance(value, valid_lists): if isinstance(value[0], valid_lists): fd.write("{}\n".format(key)) for dim in value: write_list(fd, dim, unit) else: fd.write("{}\n".format(key)) write_list(fd, value, unit) else: if unit is None: fd.write("{} {}\n".format(key, value)) else: fd.write("{} {} {}\n".format(key, value, unit)) if param['raw'] is not None: if isinstance(param['raw'], str): raise TypeError('The raw parameter is a single string; expected ' 'a sequence of lines') for line in param['raw']: if isinstance(line, tuple): fd.write(' '.join(['%s' % x for x in line]) + '\n') else: fd.write('%s\n' % line) fd.write('#Definition of the unit cell\n') fd.write('acell\n') fd.write('%.14f %.14f %.14f Angstrom\n' % (1.0, 1.0, 1.0)) fd.write('rprim\n') if atoms.cell.rank != 3: raise RuntimeError('Abinit requires a 3D cell, but cell is {}' .format(atoms.cell)) for v in atoms.cell: fd.write('%.14f %.14f %.14f\n' % tuple(v)) fd.write('chkprim 0 # Allow non-primitive cells\n') fd.write('#Definition of the atom types\n') fd.write('ntypat %d\n' % (len(species))) fd.write('znucl {}\n'.format(' '.join(str(Z) for Z in species))) fd.write('#Enumerate different atomic species\n') fd.write('typat') fd.write('\n') types = [] for Z in atoms.numbers: for n, Zs in enumerate(species): if Z == Zs: types.append(n + 1) n_entries_int = 20 # integer entries per line for n, type in enumerate(types): fd.write(' %d' % (type)) if n > 1 and ((n % n_entries_int) == 1): fd.write('\n') fd.write('\n') if pseudos is not None: listing = ',\n'.join(pseudos) line = f'pseudos "{listing}"\n' fd.write(line) fd.write('#Definition of the atoms\n') fd.write('xcart\n') for pos in atoms.positions / Bohr: fd.write('%.14f %.14f %.14f\n' % tuple(pos)) fd.write('chkexit 1 # abinit.exit file in the running ' 'directory terminates after the current SCF\n')
def write_list(fd, value, unit): for element in value: fd.write("{} ".format(element)) if unit is not None: fd.write("{}".format(unit)) fd.write("\n") def read_stress(fd): # sigma(1 1)= 4.02063464E-04 sigma(3 2)= 0.00000000E+00 # sigma(2 2)= 4.02063464E-04 sigma(3 1)= 0.00000000E+00 # sigma(3 3)= 4.02063464E-04 sigma(2 1)= 0.00000000E+00 pat = re.compile(r'\s*sigma\(\d \d\)=\s*(\S+)\s*sigma\(\d \d\)=\s*(\S+)') stress = np.empty(6) for i in range(3): line = next(fd) m = pat.match(line) if m is None: # Not a real value error. What should we raise? raise ValueError('Line {!r} does not match stress pattern {!r}' .format(line, pat)) s1, s2 = m.group(1, 2) stress[i] = float(m.group(1)) stress[i + 3] = float(m.group(2)) unit = Hartree / Bohr**3 return stress * unit def consume_multiline(fd, headerline, nvalues, dtype): """Parse abinit-formatted "header + values" sections. Example: typat 1 1 1 1 1 1 1 1 1 """ tokens = headerline.split() assert tokens[0].isalpha() values = tokens[1:] while len(values) < nvalues: line = next(fd) values.extend(line.split()) assert len(values) == nvalues return np.array(values).astype(dtype)
[docs]def read_abinit_out(fd): results = {} def skipto(string): for line in fd: if string in line: return line raise RuntimeError('Not found: {}'.format(string)) line = skipto('Version') m = re.match(r'\.*?Version\s+(\S+)\s+of ABINIT', line) assert m is not None version = m.group(1) results['version'] = version use_v9_format = int(version.split('.', 1)[0]) >= 9 shape_vars = {} skipto('echo values of preprocessed input variables') for line in fd: if '===============' in line: break tokens = line.split() if not tokens: continue for key in ['natom', 'nkpt', 'nband', 'ntypat']: if tokens[0] == key: shape_vars[key] = int(tokens[1]) if line.lstrip().startswith('typat'): # Avoid matching ntypat types = consume_multiline(fd, line, shape_vars['natom'], int) if 'znucl' in line: znucl = consume_multiline(fd, line, shape_vars['ntypat'], float) if 'rprim' in line: cell = consume_multiline(fd, line, 9, float) cell = cell.reshape(3, 3) natoms = shape_vars['natom'] # Skip ahead to results: for line in fd: if 'was not enough scf cycles to converge' in line: raise RuntimeError(line) if 'iterations are completed or convergence reached' in line: break else: raise RuntimeError('Cannot find results section') def read_array(fd, nlines): arr = [] for i in range(nlines): line = next(fd) arr.append(line.split()[1:]) arr = np.array(arr).astype(float) return arr if use_v9_format: energy_header = '--- !EnergyTerms' total_energy_name = 'total_energy_eV' def parse_energy(line): return float(line.split(':')[1].strip()) else: energy_header = 'Components of total free energy (in Hartree) :' total_energy_name = '>>>>>>>>> Etotal' def parse_energy(line): return float(line.rsplit('=', 2)[1]) * Hartree for line in fd: if 'cartesian coordinates (angstrom) at end' in line: positions = read_array(fd, natoms) if 'cartesian forces (eV/Angstrom) at end' in line: results['forces'] = read_array(fd, natoms) if 'Cartesian components of stress tensor (hartree/bohr^3)' in line: results['stress'] = read_stress(fd) if line.strip() == energy_header: # Header not to be confused with EnergyTermsDC, # therefore we don't use .startswith() energy = None for line in fd: # Which of the listed energies should we include? if total_energy_name in line: energy = parse_energy(line) break if energy is None: raise RuntimeError('No energy found in output') results['energy'] = results['free_energy'] = energy if 'END DATASET(S)' in line: break znucl_int = znucl.astype(int) znucl_int[znucl_int != znucl] = 0 # (Fractional Z) numbers = znucl_int[types - 1] atoms = Atoms(numbers=numbers, positions=positions, cell=cell, pbc=True) results['atoms'] = atoms return results
def match_kpt_header(line): headerpattern = (r'\s*kpt#\s*\S+\s*' r'nband=\s*(\d+),\s*' r'wtk=\s*(\S+?),\s*' r'kpt=\s*(\S+)+\s*(\S+)\s*(\S+)') m = re.match(headerpattern, line) assert m is not None, line nbands = int(m.group(1)) weight = float(m.group(2)) kvector = np.array(m.group(3, 4, 5)).astype(float) return nbands, weight, kvector def read_eigenvalues_for_one_spin(fd, nkpts): kpoint_weights = [] kpoint_coords = [] eig_kn = [] for ikpt in range(nkpts): header = next(fd) nbands, weight, kvector = match_kpt_header(header) kpoint_coords.append(kvector) kpoint_weights.append(weight) eig_n = [] while len(eig_n) < nbands: line = next(fd) tokens = line.split() values = np.array(tokens).astype(float) * Hartree eig_n.extend(values) assert len(eig_n) == nbands eig_kn.append(eig_n) assert nbands == len(eig_kn[0]) kpoint_weights = np.array(kpoint_weights) kpoint_coords = np.array(kpoint_coords) eig_kn = np.array(eig_kn) return kpoint_coords, kpoint_weights, eig_kn def read_eig(fd): line = next(fd) results = {} m = re.match(r'\s*Fermi \(or HOMO\) energy \(hartree\)\s*=\s*(\S+)', line) if m is not None: results['fermilevel'] = float(m.group(1)) * Hartree line = next(fd) nspins = 1 m = re.match(r'\s*Magnetization \(Bohr magneton\)=\s*(\S+)', line) if m is not None: nspins = 2 magmom = float(m.group(1)) results['magmom'] = magmom line = next(fd) if 'Total spin up' in line: assert nspins == 2 line = next(fd) m = re.match(r'\s*Eigenvalues \(hartree\) for nkpt\s*=' r'\s*(\S+)\s*k\s*points', line) if 'SPIN' in line or 'spin' in line: # If using spinpol with fixed magmoms, we don't get the magmoms # listed before now. nspins = 2 assert m is not None nkpts = int(m.group(1)) eig_skn = [] kpts, weights, eig_kn = read_eigenvalues_for_one_spin(fd, nkpts) nbands = eig_kn.shape[1] eig_skn.append(eig_kn) if nspins == 2: line = next(fd) assert 'SPIN DOWN' in line _, _, eig_kn = read_eigenvalues_for_one_spin(fd, nkpts) assert eig_kn.shape == (nkpts, nbands) eig_skn.append(eig_kn) eig_skn = np.array(eig_skn) eigshape = (nspins, nkpts, nbands) assert eig_skn.shape == eigshape, (eig_skn.shape, eigshape) results['ibz_kpoints'] = kpts results['kpoint_weights'] = weights results['eigenvalues'] = eig_skn return results def write_files_file(fd, label, ppp_list): """Write files-file, the file which tells abinit about other files.""" prefix = label.rsplit('/', 1)[-1] fd.write('%s\n' % (prefix + '.in')) # input fd.write('%s\n' % (prefix + '.txt')) # output fd.write('%s\n' % (prefix + 'i')) # input fd.write('%s\n' % (prefix + 'o')) # output fd.write('%s\n' % (prefix + '.abinit')) # Provide the psp files for ppp in ppp_list: fd.write('%s\n' % (ppp)) # psp file path def get_default_abinit_pp_paths(): return os.environ.get('ABINIT_PP_PATH', '.').split(':') abinit_input_version_warning = """\ Abinit input format has changed in Abinit9. ASE will currently write inputs for Abinit8 by default. Please silence this warning passing either Abinit(v8_legacy_format=True) to write the old Abinit8 format, or False for writing the new Abinit9+ format. The default will change to Abinit9+ format from ase-3.22, and this warning will be removed. Please note that stdin to Abinit should be the .files file until version 8 but the main inputfile (conventionally abinit.in) from abinit9, which may require reconfiguring the ASE/Abinit shell command. """ def write_all_inputs(atoms, properties, parameters, pp_paths=None, raise_exception=True, label='abinit', *, v8_legacy_format=True): species = sorted(set(atoms.numbers)) if pp_paths is None: pp_paths = get_default_abinit_pp_paths() ppp = get_ppp_list(atoms, species, raise_exception=raise_exception, xc=parameters.xc, pps=parameters.pps, search_paths=pp_paths) if v8_legacy_format is None: warnings.warn(abinit_input_version_warning, FutureWarning) v8_legacy_format = True if v8_legacy_format: with open(label + '.files', 'w') as fd: write_files_file(fd, label, ppp) pseudos = None # XXX here we build the txt filename again, which is bad # (also defined in the calculator) output_filename = label + '.txt' else: pseudos = ppp # Include pseudopotentials in inputfile output_filename = label + '.abo' # Abinit will write to label.txtA if label.txt already exists, # so we remove it if it's there: if os.path.isfile(output_filename): os.remove(output_filename) parameters.write(label + '.ase') with open(label + '.in', 'w') as fd: write_abinit_in(fd, atoms, param=parameters, species=species, pseudos=pseudos) def read_ase_and_abinit_inputs(label): with open(label + '.in') as fd: atoms = read_abinit_in(fd) parameters = Parameters.read(label + '.ase') return atoms, parameters def read_results(label, textfilename): # filename = label + '.txt' results = {} with open(textfilename) as fd: dct = read_abinit_out(fd) results.update(dct) # The eigenvalues section in the main file is shortened to # a limited number of kpoints. We read the complete one from # the EIG file then: with open('{}o_EIG'.format(label)) as fd: dct = read_eig(fd) results.update(dct) return results def get_ppp_list(atoms, species, raise_exception, xc, pps, search_paths): ppp_list = [] xcname = 'GGA' if xc != 'LDA' else 'LDA' for Z in species: number = abs(Z) symbol = chemical_symbols[number] names = [] for s in [symbol, symbol.lower()]: for xcn in [xcname, xcname.lower()]: if pps in ['paw']: hghtemplate = '%s-%s-%s.paw' # E.g. "H-GGA-hard-uspp.paw" names.append(hghtemplate % (s, xcn, '*')) names.append('%s[.-_]*.paw' % s) elif pps in ['pawxml']: hghtemplate = '%s.%s%s.xml' # E.g. "H.GGA_PBE-JTH.xml" names.append(hghtemplate % (s, xcn, '*')) names.append('%s[.-_]*.xml' % s) elif pps in ['hgh.k']: hghtemplate = '%s-q%s.hgh.k' # E.g. "Co-q17.hgh.k" names.append(hghtemplate % (s, '*')) names.append('%s[.-_]*.hgh.k' % s) names.append('%s[.-_]*.hgh' % s) elif pps in ['tm']: hghtemplate = '%d%s%s.pspnc' # E.g. "44ru.pspnc" names.append(hghtemplate % (number, s, '*')) names.append('%s[.-_]*.pspnc' % s) elif pps in ['hgh', 'hgh.sc']: hghtemplate = '%d%s.%s.hgh' # E.g. "42mo.6.hgh" # There might be multiple files with different valence # electron counts, so we must choose between # the ordinary and the semicore versions for some elements. # # Therefore we first use glob to get all relevant files, # then pick the correct one afterwards. names.append(hghtemplate % (number, s, '*')) names.append('%d%s%s.hgh' % (number, s, '*')) names.append('%s[.-_]*.hgh' % s) else: # default extension names.append('%02d-%s.%s.%s' % (number, s, xcn, pps)) names.append('%02d[.-_]%s*.%s' % (number, s, pps)) names.append('%02d%s*.%s' % (number, s, pps)) names.append('%s[.-_]*.%s' % (s, pps)) found = False for name in names: # search for file names possibilities for path in search_paths: # in all available directories filenames = glob(join(path, name)) if not filenames: continue if pps == 'paw': # warning: see download.sh in # abinit-pseudopotentials*tar.gz for additional # information! # # XXXX This is probably buggy, max(filenames) uses # an lexicographic order so 14 < 8, and it's # untested so if I change it I'm sure things will # just be inconsistent. --askhl filenames[0] = max(filenames) # Semicore or hard elif pps == 'hgh': # Lowest valence electron count filenames[0] = min(filenames) elif pps == 'hgh.k': # Semicore - highest electron count filenames[0] = max(filenames) elif pps == 'tm': # Semicore - highest electron count filenames[0] = max(filenames) elif pps == 'hgh.sc': # Semicore - highest electron count filenames[0] = max(filenames) if filenames: found = True ppp_list.append(filenames[0]) break if found: break if not found: ppp_list.append("Provide {}.{}.{}?".format(symbol, '*', pps)) if raise_exception: msg = ('Could not find {} pseudopotential {} for {}' .format(xcname.lower(), pps, symbol)) raise RuntimeError(msg) return ppp_list