Coverage for ase / io / vasp.py: 90.10%
515 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
1# fmt: off
3"""
4This module contains functionality for reading and writing an ASE
5Atoms object in VASP POSCAR format.
7"""
8from __future__ import annotations
10import re
11from pathlib import Path
12from typing import TextIO
14import numpy as np
16from ase import Atoms
17from ase.constraints import FixAtoms, FixedLine, FixedPlane, FixScaled
18from ase.io import ParseError
19from ase.io.formats import string2index
20from ase.io.utils import ImageIterator
21from ase.symbols import Symbols
22from ase.units import Ang, fs
23from ase.utils import reader, writer
25from .vasp_parsers import vasp_outcar_parsers as vop
27__all__ = [
28 'read_vasp', 'read_vasp_out', 'iread_vasp_out', 'read_vasp_xdatcar',
29 'read_vasp_xml', 'write_vasp', 'write_vasp_xdatcar'
30]
32EVTOJ = 1.60217733E-19 # VASP constant.F
35def parse_poscar_scaling_factor(line: str) -> np.ndarray:
36 """Parse scaling factor(s) in the second line in POSCAR/CONTCAR.
38 This can also be one negative number or three positive numbers.
40 https://www.vasp.at/wiki/index.php/POSCAR#Full_format_specification
42 """
43 scale = []
44 for _ in line.split()[:3]:
45 try:
46 scale.append(float(_))
47 except ValueError:
48 break
49 if len(scale) not in {1, 3}:
50 raise RuntimeError('The number of scaling factors must be 1 or 3.')
51 if len(scale) == 3 and any(_ < 0.0 for _ in scale):
52 raise RuntimeError('All three scaling factors must be positive.')
53 return np.array(scale)
56def get_atomtypes(fname):
57 """Given a file name, get the atomic symbols.
59 The function can get this information from OUTCAR and POTCAR
60 format files. The files can also be compressed with gzip or
61 bzip2.
63 """
64 fpath = Path(fname)
66 atomtypes = []
67 atomtypes_alt = []
68 if fpath.suffix == '.gz':
69 import gzip
70 opener = gzip.open
71 elif fpath.suffix == '.bz2':
72 import bz2
73 opener = bz2.BZ2File
74 else:
75 opener = open
76 with opener(fpath) as fd:
77 for line in fd:
78 if 'TITEL' in line:
79 atomtypes.append(line.split()[3].split('_')[0].split('.')[0])
80 elif 'POTCAR:' in line:
81 atomtypes_alt.append(
82 line.split()[2].split('_')[0].split('.')[0])
84 if len(atomtypes) == 0 and len(atomtypes_alt) > 0:
85 # old VASP doesn't echo TITEL, but all versions print out species
86 # lines preceded by "POTCAR:", twice
87 if len(atomtypes_alt) % 2 != 0:
88 raise ParseError(
89 f'Tried to get atom types from {len(atomtypes_alt)}'
90 '"POTCAR": lines in OUTCAR, but expected an even number'
91 )
92 atomtypes = atomtypes_alt[0:len(atomtypes_alt) // 2]
94 return atomtypes
97def atomtypes_outpot(posfname, numsyms):
98 """Try to retrieve chemical symbols from OUTCAR or POTCAR
100 If getting atomtypes from the first line in POSCAR/CONTCAR fails, it might
101 be possible to find the data in OUTCAR or POTCAR, if these files exist.
103 posfname -- The filename of the POSCAR/CONTCAR file we're trying to read
105 numsyms -- The number of symbols we must find
107 """
108 posfpath = Path(posfname)
110 # Check files with exactly same path except POTCAR/OUTCAR instead
111 # of POSCAR/CONTCAR.
112 fnames = [posfpath.with_name('POTCAR'),
113 posfpath.with_name('OUTCAR')]
114 # Try the same but with compressed files
115 fsc = []
116 for fnpath in fnames:
117 fsc.append(fnpath.parent / (fnpath.name + '.gz'))
118 fsc.append(fnpath.parent / (fnpath.name + '.bz2'))
119 for f in fsc:
120 fnames.append(f)
121 # Code used to try anything with POTCAR or OUTCAR in the name
122 # but this is no longer supported
124 tried = []
125 for fn in fnames:
126 if fn in posfpath.parent.iterdir():
127 tried.append(fn)
128 at = get_atomtypes(fn)
129 if len(at) == numsyms:
130 return at
132 raise ParseError('Could not determine chemical symbols. Tried files ' +
133 str(tried))
136def get_atomtypes_from_formula(formula):
137 """Return atom types from chemical formula (optionally prepended
138 with and underscore).
139 """
140 from ase.symbols import string2symbols
141 symbols = string2symbols(formula.split('_')[0])
142 atomtypes = [symbols[0]]
143 for s in symbols[1:]:
144 if s != atomtypes[-1]:
145 atomtypes.append(s)
146 return atomtypes
149@reader
150def read_vasp(fd):
151 """Import POSCAR/CONTCAR type file.
153 Reads unitcell, atom positions and constraints from the POSCAR/CONTCAR
154 file and tries to read atom types from POSCAR/CONTCAR header, if this
155 fails the atom types are read from OUTCAR or POTCAR file.
156 """
157 atoms = read_vasp_configuration(fd)
158 velocity_init_line = fd.readline()
159 if velocity_init_line.strip() and velocity_init_line[0].lower() == 'l':
160 read_lattice_velocities(fd)
161 velocities = read_velocities_if_present(fd, len(atoms))
162 if velocities is not None:
163 atoms.set_velocities(velocities)
164 return atoms
167def read_vasp_configuration(fd):
168 """Read common POSCAR/CONTCAR/CHGCAR/CHG quantities and return Atoms."""
169 from ase.data import chemical_symbols
171 # The first line is in principle a comment line, however in VASP
172 # 4.x a common convention is to have it contain the atom symbols,
173 # eg. "Ag Ge" in the same order as later in the file (and POTCAR
174 # for the full vasp run). In the VASP 5.x format this information
175 # is found on the fifth line. Thus we save the first line and use
176 # it in case we later detect that we're reading a VASP 4.x format
177 # file.
178 line1 = fd.readline()
180 scale = parse_poscar_scaling_factor(fd.readline())
182 # Now the lattice vectors
183 cell = np.array([fd.readline().split()[:3] for _ in range(3)], dtype=float)
184 # Negative scaling factor corresponds to the cell volume.
185 if scale[0] < 0.0:
186 scale = np.cbrt(-1.0 * scale / np.linalg.det(cell))
187 cell *= scale # This works for both one and three scaling factors.
189 # Number of atoms. Again this must be in the same order as
190 # in the first line
191 # or in the POTCAR or OUTCAR file
192 atom_symbols = []
193 numofatoms = fd.readline().split()
194 # Check whether we have a VASP 4.x or 5.x format file. If the
195 # format is 5.x, use the fifth line to provide information about
196 # the atomic symbols.
197 vasp5 = False
198 try:
199 int(numofatoms[0])
200 except ValueError:
201 vasp5 = True
202 atomtypes = numofatoms
203 numofatoms = fd.readline().split()
205 # check for comments in numofatoms line and get rid of them if necessary
206 commentcheck = np.array(['!' in s for s in numofatoms])
207 if commentcheck.any():
208 # only keep the elements up to the first including a '!':
209 numofatoms = numofatoms[:np.arange(len(numofatoms))[commentcheck][0]]
211 if not vasp5:
212 # Split the comment line (first in the file) into words and
213 # try to compose a list of chemical symbols
214 from ase.formula import Formula
215 atomtypes = []
216 for word in line1.split():
217 word_without_delims = re.sub(r"-|_|,|\.|=|[0-9]|^", "", word)
218 if len(word_without_delims) < 1:
219 continue
220 try:
221 atomtypes.extend(list(Formula(word_without_delims)))
222 except ValueError:
223 # print(atomtype, e, 'is comment')
224 pass
225 # Now the list of chemical symbols atomtypes must be formed.
226 # For example: atomtypes = ['Pd', 'C', 'O']
228 numsyms = len(numofatoms)
229 if len(atomtypes) < numsyms:
230 # First line in POSCAR/CONTCAR didn't contain enough symbols.
232 # Sometimes the first line in POSCAR/CONTCAR is of the form
233 # "CoP3_In-3.pos". Check for this case and extract atom types
234 if len(atomtypes) == 1 and '_' in atomtypes[0]:
235 atomtypes = get_atomtypes_from_formula(atomtypes[0])
236 else:
237 atomtypes = atomtypes_outpot(fd.name, numsyms)
238 else:
239 try:
240 for atype in atomtypes[:numsyms]:
241 if atype not in chemical_symbols:
242 raise KeyError
243 except KeyError:
244 atomtypes = atomtypes_outpot(fd.name, numsyms)
246 for i, num in enumerate(numofatoms):
247 numofatoms[i] = int(num)
248 atom_symbols.extend(numofatoms[i] * [atomtypes[i]])
250 # Check if Selective dynamics is switched on
251 sdyn = fd.readline()
252 selective_dynamics = sdyn[0].lower() == 's'
254 # Check if atom coordinates are cartesian or direct
255 if selective_dynamics:
256 ac_type = fd.readline()
257 else:
258 ac_type = sdyn
259 cartesian = ac_type[0].lower() in ['c', 'k']
260 tot_natoms = sum(numofatoms)
261 atoms_pos = np.empty((tot_natoms, 3))
262 if selective_dynamics:
263 selective_flags = np.empty((tot_natoms, 3), dtype=bool)
264 for atom in range(tot_natoms):
265 ac = fd.readline().split()
266 atoms_pos[atom] = [float(_) for _ in ac[0:3]]
267 if selective_dynamics:
268 selective_flags[atom] = [_ == 'F' for _ in ac[3:6]]
270 atoms = Atoms(symbols=atom_symbols, cell=cell, pbc=True)
271 if cartesian:
272 atoms_pos *= scale
273 atoms.set_positions(atoms_pos)
274 else:
275 atoms.set_scaled_positions(atoms_pos)
276 if selective_dynamics:
277 set_constraints(atoms, selective_flags)
279 return atoms
282def read_lattice_velocities(fd):
283 """
284 Read lattice velocities and vectors from POSCAR/CONTCAR.
285 As lattice velocities are not yet implemented in ASE, this function just
286 throws away these lines.
287 """
288 fd.readline() # initialization state
289 for _ in range(3): # lattice velocities
290 fd.readline()
291 for _ in range(3): # lattice vectors
292 fd.readline()
293 fd.readline() # get rid of 1 empty line below if it exists
296def read_velocities_if_present(fd, natoms) -> np.ndarray | None:
297 """Read velocities from POSCAR/CONTCAR if present, return in ASE units."""
298 # Check if it is the velocities block or the MD extra block
299 words = fd.readline().split()
300 if len(words) <= 1: # MD extra block or end of file
301 return None
302 atoms_vel = np.empty((natoms, 3))
303 atoms_vel[0] = (float(words[0]), float(words[1]), float(words[2]))
304 for atom in range(1, natoms):
305 words = fd.readline().split()
306 assert len(words) == 3
307 atoms_vel[atom] = (float(words[0]), float(words[1]), float(words[2]))
309 # unit conversion from Angstrom/fs to ASE units
310 return atoms_vel * (Ang / fs)
313def set_constraints(atoms: Atoms, selective_flags: np.ndarray):
314 """Set constraints based on selective_flags"""
315 from ase.constraints import FixAtoms, FixConstraint, FixScaled
317 constraints: list[FixConstraint] = []
318 indices = []
319 for ind, sflags in enumerate(selective_flags):
320 if sflags.any() and not sflags.all():
321 constraints.append(FixScaled(ind, sflags, atoms.get_cell()))
322 elif sflags.all():
323 indices.append(ind)
324 if indices:
325 constraints.append(FixAtoms(indices))
326 if constraints:
327 atoms.set_constraint(constraints)
330def iread_vasp_out(filename, index=-1):
331 """Import OUTCAR type file, as a generator."""
332 it = ImageIterator(vop.outcarchunks)
333 return it(filename, index=index)
336@reader
337def read_vasp_out(filename='OUTCAR', index=-1):
338 """Import OUTCAR type file.
340 Reads unitcell, atom positions, energies, and forces from the OUTCAR file
341 and attempts to read constraints (if any) from CONTCAR/POSCAR, if present.
342 """
343 # "filename" is actually a file-descriptor thanks to @reader
344 g = iread_vasp_out(filename, index=index)
345 # Code borrowed from formats.py:read
346 if isinstance(index, (slice, str)):
347 # Return list of atoms
348 return list(g)
349 else:
350 # Return single atoms object
351 return next(g)
354@reader
355def read_vasp_xdatcar(filename='XDATCAR', index=-1):
356 """Import XDATCAR file.
358 Parameters
359 ----------
360 index : int or slice or str
361 Which frame(s) to read. The default is -1 (last frame).
362 See :func:`ase.io.read` for details.
364 Notes
365 -----
366 Constraints ARE NOT stored in the XDATCAR, and as such, Atoms objects
367 retrieved from the XDATCAR will not have constraints.
368 """
369 fd = filename # @reader decorator ensures this is a file descriptor
370 images = []
372 cell = np.eye(3)
373 atomic_formula = ''
375 while True:
376 comment_line = fd.readline()
377 if "Direct configuration=" not in comment_line:
378 try:
379 lattice_constant = float(fd.readline())
380 except Exception:
381 # XXX: When would this happen?
382 break
384 xx = [float(x) for x in fd.readline().split()]
385 yy = [float(y) for y in fd.readline().split()]
386 zz = [float(z) for z in fd.readline().split()]
387 cell = np.array([xx, yy, zz]) * lattice_constant
389 symbols = fd.readline().split()
390 numbers = [int(n) for n in fd.readline().split()]
391 total = sum(numbers)
393 atomic_formula = ''.join(f'{sym:s}{numbers[n]:d}'
394 for n, sym in enumerate(symbols))
396 fd.readline()
398 coords = [np.array(fd.readline().split(), float) for _ in range(total)]
400 image = Atoms(atomic_formula, cell=cell, pbc=True)
401 image.set_scaled_positions(np.array(coords))
402 images.append(image)
404 if index is None:
405 index = -1
407 if isinstance(index, str):
408 index = string2index(index)
410 return images[index]
413def __get_xml_parameter(par):
414 """An auxiliary function that enables convenient extraction of
415 parameter values from a vasprun.xml file with proper type
416 handling.
418 """
419 def to_bool(b):
420 if b == 'T':
421 return True
422 else:
423 return False
425 to_type = {'int': int, 'logical': to_bool, 'string': str, 'float': float}
427 text = par.text
428 if text is None:
429 text = ''
431 # Float parameters do not have a 'type' attrib
432 var_type = to_type[par.attrib.get('type', 'float')]
434 try:
435 if par.tag == 'v':
436 return list(map(var_type, text.split()))
437 else:
438 return var_type(text.strip())
439 except ValueError:
440 # Vasp can sometimes write "*****" due to overflow
441 return None
444def read_vasp_xml(filename='vasprun.xml', index=-1):
445 """Parse vasprun.xml file.
447 Reads unit cell, atom positions, energies, forces, and constraints
448 from vasprun.xml file
450 Examples
451 --------
452 >>> import ase.io
453 >>> ase.io.write("out.traj", ase.io.read("vasprun.xml", index=":"))
454 """
456 import xml.etree.ElementTree as ET
457 from collections import OrderedDict
459 from ase.calculators.singlepoint import (
460 SinglePointDFTCalculator,
461 SinglePointKPoint,
462 )
463 from ase.units import GPa
465 tree = ET.iterparse(filename, events=['start', 'end'])
467 atoms_init = None
468 calculation = []
469 ibz_kpts = None
470 kpt_weights = None
471 parameters = OrderedDict()
473 try:
474 for event, elem in tree:
476 if event == 'end':
477 if elem.tag == 'kpoints':
478 for subelem in elem.iter(tag='generation'):
479 kpts_params = OrderedDict()
480 parameters['kpoints_generation'] = kpts_params
481 for par in subelem.iter():
482 if par.tag in ['v', 'i'] and "name" in par.attrib:
483 parname = par.attrib['name'].lower()
484 kpts_params[parname] = __get_xml_parameter(par)
486 kpts = elem.findall("varray[@name='kpointlist']/v")
487 ibz_kpts = np.zeros((len(kpts), 3))
489 for i, kpt in enumerate(kpts):
490 ibz_kpts[i] = [float(val) for val in kpt.text.split()]
492 kpt_weights = elem.findall('varray[@name="weights"]/v')
493 kpt_weights = [float(val.text) for val in kpt_weights]
495 elif elem.tag == 'parameters':
496 for par in elem.iter():
497 if par.tag in ['v', 'i']:
498 parname = par.attrib['name'].lower()
499 parameters[parname] = __get_xml_parameter(par)
501 elif elem.tag == 'atominfo':
502 species = []
504 for entry in elem.find("array[@name='atoms']/set"):
505 species.append(entry[0].text.strip())
507 natoms = len(species)
509 elif (elem.tag == 'structure'
510 and elem.attrib.get('name') == 'initialpos'):
511 cell_init = np.zeros((3, 3), dtype=float)
513 for i, v in enumerate(
514 elem.find("crystal/varray[@name='basis']")):
515 cell_init[i] = np.array(
516 [float(val) for val in v.text.split()])
518 scpos_init = np.zeros((natoms, 3), dtype=float)
520 for i, v in enumerate(
521 elem.find("varray[@name='positions']")):
522 scpos_init[i] = np.array(
523 [float(val) for val in v.text.split()])
525 constraints = []
526 fixed_indices = []
528 for i, entry in enumerate(
529 elem.findall("varray[@name='selective']/v")):
530 flags = (np.array(
531 entry.text.split() == np.array(['F', 'F', 'F'])))
532 if flags.all():
533 fixed_indices.append(i)
534 elif flags.any():
535 constraints.append(FixScaled(i, flags, cell_init))
537 if fixed_indices:
538 constraints.append(FixAtoms(fixed_indices))
540 atoms_init = Atoms(species,
541 cell=cell_init,
542 scaled_positions=scpos_init,
543 constraint=constraints,
544 pbc=True)
546 elif elem.tag == 'dipole':
547 dblock = elem.find('v[@name="dipole"]')
548 if dblock is not None:
549 dipole = np.array(
550 [float(val) for val in dblock.text.split()])
552 elif event == 'start' and elem.tag == 'calculation':
553 calculation.append(elem)
555 except ET.ParseError as parse_error:
556 if atoms_init is None:
557 raise parse_error
558 if calculation and calculation[-1].find("energy") is None:
559 calculation = calculation[:-1]
560 if not calculation:
561 yield atoms_init
563 if calculation:
564 if isinstance(index, int):
565 steps = [calculation[index]]
566 else:
567 steps = calculation[index]
568 else:
569 steps = []
571 for step in steps:
572 cell = np.zeros((3, 3), dtype=float)
573 for i, vector in enumerate(
574 step.find('structure/crystal/varray[@name="basis"]')):
575 cell[i] = np.array([float(val) for val in vector.text.split()])
576 volume = np.linalg.det(cell)
578 free_energy = float(step.find('energy/i[@name="e_fr_energy"]').text)
580 # https://gitlab.com/ase/ase/-/merge_requests/2685
581 # e_fr_energy in calculation/energy is actually an enthalpy including
582 # the PV term, unlike that in /calculation/scstep/energy or in OUTCAR,
583 # and therefore we need to subtract the PV term.
584 pressure = parameters.get('pstress', 0.0)
585 pressure *= 1e-22 / EVTOJ # kbar -> eV/A3
586 free_energy -= pressure * volume
588 # Workaround for VASP bug, e_0_energy contains the wrong value
589 # in calculation/energy, but calculation/scstep/energy does not
590 # include classical VDW corrections. So, first calculate
591 # e_0_energy - e_fr_energy from calculation/scstep/energy, then
592 # apply that correction to e_fr_energy from calculation/energy.
593 lastscf = step.findall('scstep/energy')[-1]
594 dipoles = step.findall('scstep/dipole')
595 if dipoles:
596 lastdipole = dipoles[-1]
597 else:
598 lastdipole = None
600 de = (float(lastscf.find('i[@name="e_0_energy"]').text) -
601 float(lastscf.find('i[@name="e_fr_energy"]').text))
603 energy = free_energy + de
605 scpos = np.zeros((natoms, 3), dtype=float)
606 for i, vector in enumerate(
607 step.find('structure/varray[@name="positions"]')):
608 scpos[i] = np.array([float(val) for val in vector.text.split()])
610 forces = None
611 fblocks = step.find('varray[@name="forces"]')
612 if fblocks is not None:
613 forces = np.zeros((natoms, 3), dtype=float)
614 for i, vector in enumerate(fblocks):
615 forces[i] = np.array(
616 [float(val) for val in vector.text.split()])
618 stress = None
619 sblocks = step.find('varray[@name="stress"]')
620 if sblocks is not None:
621 stress = np.zeros((3, 3), dtype=float)
622 for i, vector in enumerate(sblocks):
623 stress[i] = np.array(
624 [float(val) for val in vector.text.split()])
625 stress *= -0.1 * GPa
626 stress = stress.reshape(9)[[0, 4, 8, 5, 2, 1]]
628 dipole = None
629 if lastdipole is not None:
630 dblock = lastdipole.find('v[@name="dipole"]')
631 if dblock is not None:
632 dipole = np.zeros((1, 3), dtype=float)
633 dipole = np.array([float(val) for val in dblock.text.split()])
635 dblock = step.find('dipole/v[@name="dipole"]')
636 if dblock is not None:
637 dipole = np.zeros((1, 3), dtype=float)
638 dipole = np.array([float(val) for val in dblock.text.split()])
640 efermi = step.find('dos/i[@name="efermi"]')
641 if efermi is not None:
642 efermi = float(efermi.text)
644 kpoints = []
645 for ikpt in range(1, len(ibz_kpts) + 1):
646 kblocks = step.findall(
647 'eigenvalues/array/set/set/set[@comment="kpoint %d"]' % ikpt)
648 if kblocks is not None:
649 for spin, kpoint in enumerate(kblocks):
650 eigenvals = kpoint.findall('r')
651 eps_n = np.zeros(len(eigenvals))
652 f_n = np.zeros(len(eigenvals))
653 for j, val in enumerate(eigenvals):
654 val = val.text.split()
655 eps_n[j] = float(val[0])
656 f_n[j] = float(val[1])
657 if len(kblocks) == 1:
658 f_n *= 2
659 kpoints.append(
660 SinglePointKPoint(kpt_weights[ikpt - 1], spin, ikpt,
661 eps_n, f_n))
662 if len(kpoints) == 0:
663 kpoints = None
665 # DFPT properties
666 # dielectric tensor
667 dielectric_tensor = None
668 sblocks = step.find('varray[@name="dielectric_dft"]')
669 if sblocks is not None:
670 dielectric_tensor = np.zeros((3, 3), dtype=float)
671 for ii, vector in enumerate(sblocks):
672 dielectric_tensor[ii] = np.fromstring(vector.text, sep=' ')
674 # Born effective charges
675 born_charges = None
676 fblocks = step.find('array[@name="born_charges"]')
677 if fblocks is not None:
678 born_charges = np.zeros((natoms, 3, 3), dtype=float)
679 for ii, block in enumerate(fblocks[1:]): # 1. element = dimension
680 for jj, vector in enumerate(block):
681 born_charges[ii, jj] = np.fromstring(vector.text, sep=' ')
683 atoms = atoms_init.copy()
684 atoms.set_cell(cell)
685 atoms.set_scaled_positions(scpos)
686 atoms.calc = SinglePointDFTCalculator(
687 atoms,
688 energy=energy,
689 forces=forces,
690 stress=stress,
691 free_energy=free_energy,
692 ibzkpts=ibz_kpts,
693 efermi=efermi,
694 dipole=dipole,
695 dielectric_tensor=dielectric_tensor,
696 born_effective_charges=born_charges
697 )
698 atoms.calc.name = 'vasp'
699 atoms.calc.kpts = kpoints
700 atoms.calc.parameters = parameters
701 yield atoms
704@writer
705def write_vasp_xdatcar(fd, images, label=None):
706 """Write VASP MD trajectory (XDATCAR) file
708 Only Vasp 5 format is supported (for consistency with read_vasp_xdatcar)
710 Args:
711 fd (str, fp): Output file
712 images (iterable of Atoms): Atoms images to write. These must have
713 consistent atom order and lattice vectors - this will not be
714 checked.
715 label (str): Text for first line of file. If empty, default to list
716 of elements.
718 """
720 images = iter(images)
721 image = next(images)
723 if not isinstance(image, Atoms):
724 raise TypeError("images should be a sequence of Atoms objects.")
726 _write_xdatcar_header(fd, image, label)
727 _write_xdatcar_config(fd, image, index=1)
728 for i, image in enumerate(images):
729 _write_xdatcar_header(fd, image, label)
730 # Index is off by 2: 1-indexed file vs 0-indexed Python;
731 # and we already wrote the first block.
732 _write_xdatcar_config(fd, image, i + 2)
735def _write_xdatcar_header(fd, atoms, label):
736 symbol_count = _symbol_count_from_symbols(atoms.get_chemical_symbols())
738 if label is None:
739 label = ' '.join([s for s, _ in symbol_count])
740 fd.write(label + '\n')
742 # Not using lattice constants, set it to 1
743 fd.write(' 1\n')
745 float_string = '{:11.6f}'
746 for row_i in range(3):
747 fd.write(' ')
748 fd.write(' '.join(float_string.format(x) for x in atoms.cell[row_i]))
749 fd.write('\n')
751 fd.write(_symbol_count_string(symbol_count, vasp5=True))
754def _write_xdatcar_config(fd, atoms, index):
755 """Write a block of positions for XDATCAR file
757 Args:
758 fd (fd): writeable Python file descriptor
759 atoms (ase.Atoms): Atoms to write
760 index (int): configuration number written to block header
762 """
763 fd.write(f"Direct configuration={index:6d}\n")
764 float_string = '{:11.8f}'
765 scaled_positions = atoms.get_scaled_positions()
766 for row in scaled_positions:
767 fd.write(' ')
768 fd.write(' '.join([float_string.format(x) for x in row]))
769 fd.write('\n')
772def _symbol_count_from_symbols(symbols: Symbols) -> list[tuple[str, int]]:
773 """Reduce list of chemical symbols into compact VASP notation
775 Args:
776 symbols (iterable of str)
778 Returns
779 -------
780 list of pairs [(el1, c1), (el2, c2), ...]
782 Example:
783 >>> s = Atoms('Ar3NeHe2ArNe').symbols
784 >>> _symbols_count_from_symbols(s)
785 [('Ar', 3), ('Ne', 1), ('He', 2), ('Ar', 1), ('Ne', 1)]
786 """
787 sc = []
788 psym = str(symbols[0]) # we cast to str to appease mypy
789 count = 0
790 for sym in symbols:
791 if sym != psym:
792 sc.append((psym, count))
793 psym = sym
794 count = 1
795 else:
796 count += 1
798 sc.append((psym, count))
799 return sc
802@writer
803def write_vasp(
804 fd: TextIO,
805 atoms: Atoms,
806 direct: bool = False,
807 sort: bool = False,
808 symbol_count: list[tuple[str, int]] | None = None,
809 vasp5: bool = True,
810 vasp6: bool = False,
811 ignore_constraints: bool = False,
812 potential_mapping: dict | None = None
813) -> None:
814 """Method to write VASP position (POSCAR/CONTCAR) files.
816 Writes label, scalefactor, unitcell, # of various kinds of atoms,
817 positions in cartesian or scaled coordinates (Direct), and constraints
818 to file. Cartesian coordinates is default and default label is the
819 atomic species, e.g. 'C N H Cu'.
821 Args:
822 fd (TextIO): writeable Python file descriptor
823 atoms (ase.Atoms): Atoms to write
824 direct (bool): Write scaled coordinates instead of cartesian
825 sort (bool): Sort the atomic indices alphabetically by element
826 symbol_count (list of tuples of str and int, optional): Use the given
827 combination of symbols and counts instead of automatically compute
828 them
829 vasp5 (bool): Write to the VASP 5+ format, where the symbols are
830 written to file
831 vasp6 (bool): Write symbols in VASP 6 format (which allows for
832 potential type and hash)
833 ignore_constraints (bool): Ignore all constraints on `atoms`
834 potential_mapping (dict, optional): Map of symbols to potential file
835 (and hash). Only works if `vasp6=True`. See `_symbol_string_count`
837 Raises
838 ------
839 RuntimeError: raised if any of these are true:
841 1. `atoms` is not a single `ase.Atoms` object.
842 2. The cell dimensionality is lower than 3 (0D-2D)
843 3. One FixedPlane normal is not parallel to a unit cell vector
844 4. One FixedLine direction is not parallel to a unit cell vector
845 """
846 if isinstance(atoms, (list, tuple)):
847 if len(atoms) > 1:
848 raise RuntimeError(
849 'Only one atomic structure can be saved to VASP '
850 'POSCAR/CONTCAR. Several were given.'
851 )
852 else:
853 atoms = atoms[0]
855 # Check lattice vectors are finite
856 if atoms.cell.rank < 3:
857 raise RuntimeError(
858 'Lattice vectors must be finite and non-parallel. At least '
859 'one lattice length or angle is zero.'
860 )
862 # Write atomic positions in scaled or cartesian coordinates
863 if direct:
864 coord = atoms.get_scaled_positions(wrap=False)
865 else:
866 coord = atoms.positions
868 # Convert ASE constraints to VASP POSCAR constraints
869 constraints_present = atoms.constraints and not ignore_constraints
870 if constraints_present:
871 sflags = _handle_ase_constraints(atoms)
873 # Conditionally sort ordering of `atoms` alphabetically by symbols
874 if sort:
875 ind = np.argsort(atoms.symbols)
876 symbols = atoms.symbols[ind]
877 coord = coord[ind]
878 if constraints_present:
879 sflags = sflags[ind]
880 else:
881 symbols = atoms.symbols
883 # Set or create a list of (symbol, count) pairs
884 sc = symbol_count or _symbol_count_from_symbols(symbols)
886 # Write header as atomic species in `symbol_count` order
887 label = ' '.join(f'{sym:2s}' for sym, _ in sc)
888 fd.write(label + '\n')
890 # For simplicity, we write the unitcell in real coordinates, so the
891 # scaling factor is always set to 1.0.
892 fd.write(f'{1.0:19.16f}\n')
894 for vec in atoms.cell:
895 fd.write(' ' + ' '.join([f'{el:21.16f}' for el in vec]) + '\n')
897 # Write version-dependent species-and-count section
898 sc_str = _symbol_count_string(sc, vasp5, vasp6, potential_mapping)
899 fd.write(sc_str)
901 # Write POSCAR switches
902 if constraints_present:
903 fd.write('Selective dynamics\n')
905 fd.write('Direct\n' if direct else 'Cartesian\n')
907 # Write atomic positions and, if any, the cartesian constraints
908 for iatom, atom in enumerate(coord):
909 for dcoord in atom:
910 fd.write(f' {dcoord:19.16f}')
911 if constraints_present:
912 flags = ['F' if flag else 'T' for flag in sflags[iatom]]
913 fd.write(''.join([f'{f:>4s}' for f in flags]))
914 fd.write('\n')
916 # if velocities in atoms object write velocities
917 if atoms.has('momenta'):
918 cform = 3 * ' {:19.16f}' + '\n'
919 fd.write('Cartesian\n')
920 # unit conversion to Angstrom / fs
921 vel = atoms.get_velocities() / (Ang / fs)
922 for vatom in vel:
923 fd.write(cform.format(*vatom))
926def _handle_ase_constraints(atoms: Atoms) -> np.ndarray:
927 """Convert the ASE constraints on `atoms` to VASP constraints
929 Returns a boolean array with dimensions Nx3, where N is the number of
930 atoms. A value of `True` indicates that movement along the given lattice
931 vector is disallowed for that atom.
933 Args:
934 atoms (Atoms)
936 Returns
937 -------
938 boolean numpy array with dimensions Nx3
940 Raises
941 ------
942 RuntimeError: If there is a FixedPlane or FixedLine constraint, that
943 is not parallel to a cell vector.
944 """
945 sflags = np.zeros((len(atoms), 3), dtype=bool)
946 for constr in atoms.constraints:
947 if isinstance(constr, FixScaled):
948 sflags[constr.index] = constr.mask
949 elif isinstance(constr, FixAtoms):
950 sflags[constr.index] = 3 * [True]
951 elif isinstance(constr, FixedPlane):
952 # Calculate if the plane normal is parallel to a cell vector
953 mask = np.all(
954 np.abs(np.cross(constr.dir, atoms.cell)) < 1e-5, axis=1
955 )
956 if mask.sum() != 1:
957 raise RuntimeError(
958 'VASP requires that the direction of FixedPlane '
959 'constraints is parallel with one of the cell axis'
960 )
961 sflags[constr.index] = mask
962 elif isinstance(constr, FixedLine):
963 # Calculate if line is parallel to a cell vector
964 mask = np.all(
965 np.abs(np.cross(constr.dir, atoms.cell)) < 1e-5, axis=1
966 )
967 if mask.sum() != 1:
968 raise RuntimeError(
969 'VASP requires that the direction of FixedLine '
970 'constraints is parallel with one of the cell axis'
971 )
972 sflags[constr.index] = ~mask
974 return sflags
977def _symbol_count_string(
978 symbol_count: list[tuple[str, int]], vasp5: bool = True,
979 vasp6: bool = True, symbol_mapping: dict | None = None
980) -> str:
981 """Create the symbols-and-counts block for POSCAR or XDATCAR
983 Args:
984 symbol_count (list of 2-tuple): list of paired elements and counts
985 vasp5 (bool): if False, omit symbols and only write counts
986 vasp6 (bool): if True, write symbols in VASP 6 format (allows for
987 potential type and hash)
988 symbol_mapping (dict): mapping of symbols to VASP 6 symbols
990 e.g. if sc is [(Sn, 4), (S, 6)] then write for vasp 5:
991 Sn S
992 4 6
994 and for vasp 6 with mapping {'Sn': 'Sn_d_GW', 'S': 'S_GW/357d'}:
995 Sn_d_GW S_GW/357d
996 4 6
997 """
998 symbol_mapping = symbol_mapping or {}
999 out_str = ' '
1001 # Allow for VASP 6 format, i.e., specifying the pseudopotential used
1002 if vasp6:
1003 out_str += ' '.join([
1004 f"{symbol_mapping.get(s, s)[:14]:16s}" for s, _ in symbol_count
1005 ]) + "\n "
1006 out_str += ' '.join([f"{c:16d}" for _, c in symbol_count]) + '\n'
1007 return out_str
1009 # Write the species for VASP 5+
1010 if vasp5:
1011 out_str += ' '.join([f"{s:3s}" for s, _ in symbol_count]) + "\n "
1013 # Write counts line
1014 out_str += ' '.join([f"{c:3d}" for _, c in symbol_count]) + '\n'
1016 return out_str