Coverage for /builds/debichem-team/python-ase/ase/calculators/vasp/vasp_auxiliary.py: 29.74%
195 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
1import os
2import re
4import numpy as np
7def get_vasp_version(string):
8 """Extract version number from header of stdout.
10 Example::
12 >>> get_vasp_version('potato vasp.6.1.2 bumblebee')
13 '6.1.2'
15 """
16 match = re.search(r'vasp\.(\S+)', string, re.M)
17 return match.group(1)
20class VaspChargeDensity:
21 """Class for representing VASP charge density.
23 Filename is normally CHG."""
24 # Can the filename be CHGCAR? There's a povray tutorial
25 # in doc/tutorials where it's CHGCAR as of January 2021. --askhl
27 def __init__(self, filename):
28 # Instance variables
29 self.atoms = [] # List of Atoms objects
30 self.chg = [] # Charge density
31 self.chgdiff = [] # Charge density difference, if spin polarized
32 self.aug = '' # Augmentation charges, not parsed just a big string
33 self.augdiff = '' # Augmentation charge differece, is spin polarized
35 # Note that the augmentation charge is not a list, since they
36 # are needed only for CHGCAR files which store only a single
37 # image.
38 if filename is not None:
39 self.read(filename)
41 def is_spin_polarized(self):
42 if len(self.chgdiff) > 0:
43 return True
44 return False
46 def _read_chg(self, fobj, chg, volume):
47 """Read charge from file object
49 Utility method for reading the actual charge density (or
50 charge density difference) from a file object. On input, the
51 file object must be at the beginning of the charge block, on
52 output the file position will be left at the end of the
53 block. The chg array must be of the correct dimensions.
55 """
56 # VASP writes charge density as
57 # WRITE(IU,FORM) (((C(NX,NY,NZ),NX=1,NGXC),NY=1,NGYZ),NZ=1,NGZC)
58 # Fortran nested implied do loops; innermost index fastest
59 # First, just read it in
60 for zz in range(chg.shape[2]):
61 for yy in range(chg.shape[1]):
62 chg[:, yy, zz] = np.fromfile(fobj, count=chg.shape[0], sep=' ')
63 chg /= volume
65 def read(self, filename):
66 """Read CHG or CHGCAR file.
68 If CHG contains charge density from multiple steps all the
69 steps are read and stored in the object. By default VASP
70 writes out the charge density every 10 steps.
72 chgdiff is the difference between the spin up charge density
73 and the spin down charge density and is thus only read for a
74 spin-polarized calculation.
76 aug is the PAW augmentation charges found in CHGCAR. These are
77 not parsed, they are just stored as a string so that they can
78 be written again to a CHGCAR format file.
80 """
81 import ase.io.vasp as aiv
82 with open(filename) as fd:
83 self.atoms = []
84 self.chg = []
85 self.chgdiff = []
86 self.aug = ''
87 self.augdiff = ''
88 while True:
89 try:
90 atoms = aiv.read_vasp(fd)
91 except (KeyError, RuntimeError, ValueError):
92 # Probably an empty line, or we tried to read the
93 # augmentation occupancies in CHGCAR
94 break
95 fd.readline()
96 ngr = fd.readline().split()
97 ng = (int(ngr[0]), int(ngr[1]), int(ngr[2]))
98 chg = np.empty(ng)
99 self._read_chg(fd, chg, atoms.get_volume())
100 self.chg.append(chg)
101 self.atoms.append(atoms)
102 # Check if the file has a spin-polarized charge density part,
103 # and if so, read it in.
104 fl = fd.tell()
105 # First check if the file has an augmentation charge part
106 # (CHGCAR file.)
107 line1 = fd.readline()
108 if line1 == '':
109 break
110 elif line1.find('augmentation') != -1:
111 augs = [line1]
112 while True:
113 line2 = fd.readline()
114 if line2.split() == ngr:
115 self.aug = ''.join(augs)
116 augs = []
117 chgdiff = np.empty(ng)
118 self._read_chg(fd, chgdiff, atoms.get_volume())
119 self.chgdiff.append(chgdiff)
120 elif line2 == '':
121 break
122 else:
123 augs.append(line2)
124 if len(self.aug) == 0:
125 self.aug = ''.join(augs)
126 augs = []
127 else:
128 self.augdiff = ''.join(augs)
129 augs = []
130 elif line1.split() == ngr:
131 chgdiff = np.empty(ng)
132 self._read_chg(fd, chgdiff, atoms.get_volume())
133 self.chgdiff.append(chgdiff)
134 else:
135 fd.seek(fl)
137 def _write_chg(self, fobj, chg, volume, format='chg'):
138 """Write charge density
140 Utility function similar to _read_chg but for writing.
142 """
143 # Make a 1D copy of chg, must take transpose to get ordering right
144 chgtmp = chg.T.ravel()
145 # Multiply by volume
146 chgtmp = chgtmp * volume
147 # Must be a tuple to pass to string conversion
148 chgtmp = tuple(chgtmp)
149 # CHG format - 10 columns
150 if format.lower() == 'chg':
151 # Write all but the last row
152 for ii in range((len(chgtmp) - 1) // 10):
153 fobj.write(' %#11.5G %#11.5G %#11.5G %#11.5G %#11.5G\
154 %#11.5G %#11.5G %#11.5G %#11.5G %#11.5G\n' % chgtmp[ii * 10:(ii + 1) * 10])
155 # If the last row contains 10 values then write them without a
156 # newline
157 if len(chgtmp) % 10 == 0:
158 fobj.write(' %#11.5G %#11.5G %#11.5G %#11.5G %#11.5G'
159 ' %#11.5G %#11.5G %#11.5G %#11.5G %#11.5G' %
160 chgtmp[len(chgtmp) - 10:len(chgtmp)])
161 # Otherwise write fewer columns without a newline
162 else:
163 for ii in range(len(chgtmp) % 10):
164 fobj.write((' %#11.5G') %
165 chgtmp[len(chgtmp) - len(chgtmp) % 10 + ii])
166 # Other formats - 5 columns
167 else:
168 # Write all but the last row
169 for ii in range((len(chgtmp) - 1) // 5):
170 fobj.write(' %17.10E %17.10E %17.10E %17.10E %17.10E\n' %
171 chgtmp[ii * 5:(ii + 1) * 5])
172 # If the last row contains 5 values then write them without a
173 # newline
174 if len(chgtmp) % 5 == 0:
175 fobj.write(' %17.10E %17.10E %17.10E %17.10E %17.10E' %
176 chgtmp[len(chgtmp) - 5:len(chgtmp)])
177 # Otherwise write fewer columns without a newline
178 else:
179 for ii in range(len(chgtmp) % 5):
180 fobj.write((' %17.10E') %
181 chgtmp[len(chgtmp) - len(chgtmp) % 5 + ii])
182 # Write a newline whatever format it is
183 fobj.write('\n')
185 def write(self, filename, format=None):
186 """Write VASP charge density in CHG format.
188 filename: str
189 Name of file to write to.
190 format: str
191 String specifying whether to write in CHGCAR or CHG
192 format.
194 """
195 import ase.io.vasp as aiv
196 if format is None:
197 if filename.lower().find('chgcar') != -1:
198 format = 'chgcar'
199 elif filename.lower().find('chg') != -1:
200 format = 'chg'
201 elif len(self.chg) == 1:
202 format = 'chgcar'
203 else:
204 format = 'chg'
205 with open(filename, 'w') as fd:
206 for ii, chg in enumerate(self.chg):
207 if format == 'chgcar' and ii != len(self.chg) - 1:
208 continue # Write only the last image for CHGCAR
209 aiv.write_vasp(fd,
210 self.atoms[ii],
211 direct=True)
212 fd.write('\n')
213 for dim in chg.shape:
214 fd.write(' %4i' % dim)
215 fd.write('\n')
216 vol = self.atoms[ii].get_volume()
217 self._write_chg(fd, chg, vol, format)
218 if format == 'chgcar':
219 fd.write(self.aug)
220 if self.is_spin_polarized():
221 if format == 'chg':
222 fd.write('\n')
223 for dim in chg.shape:
224 fd.write(' %4i' % dim)
225 fd.write('\n') # a new line after dim is required
226 self._write_chg(fd, self.chgdiff[ii], vol, format)
227 if format == 'chgcar':
228 # a new line is always provided self._write_chg
229 fd.write(self.augdiff)
230 if format == 'chg' and len(self.chg) > 1:
231 fd.write('\n')
234class VaspDos:
235 """Class for representing density-of-states produced by VASP
237 The energies are in property self.energy
239 Site-projected DOS is accesible via the self.site_dos method.
241 Total and integrated DOS is accessible as numpy.ndarray's in the
242 properties self.dos and self.integrated_dos. If the calculation is
243 spin polarized, the arrays will be of shape (2, NDOS), else (1,
244 NDOS).
246 The self.efermi property contains the currently set Fermi
247 level. Changing this value shifts the energies.
249 """
251 def __init__(self, doscar='DOSCAR', efermi=0.0):
252 """Initialize"""
253 self._efermi = 0.0
254 self.read_doscar(doscar)
255 self.efermi = efermi
257 # we have determine the resort to correctly map ase atom index to the
258 # POSCAR.
259 self.sort = []
260 self.resort = []
261 if os.path.isfile('ase-sort.dat'):
262 with open('ase-sort.dat') as file:
263 lines = file.readlines()
264 for line in lines:
265 data = line.split()
266 self.sort.append(int(data[0]))
267 self.resort.append(int(data[1]))
269 def _set_efermi(self, efermi):
270 """Set the Fermi level."""
271 ef = efermi - self._efermi
272 self._efermi = efermi
273 self._total_dos[0, :] = self._total_dos[0, :] - ef
274 try:
275 self._site_dos[:, 0, :] = self._site_dos[:, 0, :] - ef
276 except IndexError:
277 pass
279 def _get_efermi(self):
280 return self._efermi
282 efermi = property(_get_efermi, _set_efermi, None, "Fermi energy.")
284 def _get_energy(self):
285 """Return the array with the energies."""
286 return self._total_dos[0, :]
288 energy = property(_get_energy, None, None, "Array of energies")
290 def site_dos(self, atom, orbital):
291 """Return an NDOSx1 array with dos for the chosen atom and orbital.
293 atom: int
294 Atom index
295 orbital: int or str
296 Which orbital to plot
298 If the orbital is given as an integer:
299 If spin-unpolarized calculation, no phase factors:
300 s = 0, p = 1, d = 2
301 Spin-polarized, no phase factors:
302 s-up = 0, s-down = 1, p-up = 2, p-down = 3, d-up = 4, d-down = 5
303 If phase factors have been calculated, orbitals are
304 s, py, pz, px, dxy, dyz, dz2, dxz, dx2
305 double in the above fashion if spin polarized.
307 """
308 # Correct atom index for resorting if we need to. This happens when the
309 # ase-sort.dat file exists, and self.resort is not empty.
310 if self.resort:
311 atom = self.resort[atom]
313 # Integer indexing for orbitals starts from 1 in the _site_dos array
314 # since the 0th column contains the energies
315 if isinstance(orbital, int):
316 return self._site_dos[atom, orbital + 1, :]
317 n = self._site_dos.shape[1]
319 from .vasp_data import PDOS_orbital_names_and_DOSCAR_column
320 norb = PDOS_orbital_names_and_DOSCAR_column[n]
322 return self._site_dos[atom, norb[orbital.lower()], :]
324 def _get_dos(self):
325 if self._total_dos.shape[0] == 3:
326 return self._total_dos[1, :]
327 elif self._total_dos.shape[0] == 5:
328 return self._total_dos[1:3, :]
330 dos = property(_get_dos, None, None, 'Average DOS in cell')
332 def _get_integrated_dos(self):
333 if self._total_dos.shape[0] == 3:
334 return self._total_dos[2, :]
335 elif self._total_dos.shape[0] == 5:
336 return self._total_dos[3:5, :]
338 integrated_dos = property(_get_integrated_dos, None, None,
339 'Integrated average DOS in cell')
341 def read_doscar(self, fname="DOSCAR"):
342 """Read a VASP DOSCAR file"""
343 with open(fname) as fd:
344 natoms = int(fd.readline().split()[0])
345 [fd.readline() for _ in range(4)]
346 # First we have a block with total and total integrated DOS
347 ndos = int(fd.readline().split()[2])
348 dos = []
349 for _ in range(ndos):
350 dos.append(np.array([float(x) for x in fd.readline().split()]))
351 self._total_dos = np.array(dos).T
352 # Next we have one block per atom, if INCAR contains the stuff
353 # necessary for generating site-projected DOS
354 dos = []
355 for _ in range(natoms):
356 line = fd.readline()
357 if line == '':
358 # No site-projected DOS
359 break
360 ndos = int(line.split()[2])
361 line = fd.readline().split()
362 cdos = np.empty((ndos, len(line)))
363 cdos[0] = np.array(line)
364 for nd in range(1, ndos):
365 line = fd.readline().split()
366 cdos[nd] = np.array([float(x) for x in line])
367 dos.append(cdos.T)
368 self._site_dos = np.array(dos)