Coverage for eminus/io/poscar.py: 95.77%
71 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-21 12:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-21 12:19 +0000
1# SPDX-FileCopyrightText: 2024 The eminus developers
2# SPDX-License-Identifier: Apache-2.0
3"""POSCAR file handling."""
5import time
7import numpy as np
9from eminus import backend as xp
10from eminus.logger import log
11from eminus.units import ang2bohr, bohr2ang
12from eminus.version import __version__
15def read_poscar(filename):
16 """Load atom species and positions from POSCAR files.
18 File format definition: https://www.vasp.at/wiki/index.php/POSCAR
20 Args:
21 filename: POSCAR input file path/name.
23 Returns:
24 Atom species, positions, and cell vectors.
25 """
26 if "POSCAR" not in filename:
27 filename += ".POSCAR"
29 with open(filename, encoding="utf-8") as fh:
30 lines = fh.readlines()
32 # The first line contains a comment, print it if available
33 comment = lines[0].strip()
34 if comment:
35 log.info(f'POSCAR file comment: "{comment}"')
37 # The second line contains scaling factors
38 scaling = np.asarray(lines[1].strip().split(), dtype=float)
40 # Followed by unscaled lattice coordinates, scale them here
41 a = np.empty((3, 3))
42 for i, line in enumerate(lines[2:5]):
43 a[i] = scaling * np.asarray(line.strip().split(), dtype=float)
45 # If line number six contains numbers a POTCAR file is required
46 if lines[5].strip().split()[0].isnumeric():
47 msg = "POTCAR files are not supported, provide species in the POSCAR file"
48 raise NotImplementedError(msg)
49 # Otherwise the atom species are given with their amount
50 atom = lines[5].strip().split()
51 Natom = np.asarray(lines[6].strip().split(), dtype=int)
52 # Extend the atoms by their respective amount
53 atom = [a for a, N in zip(atom, Natom) for _ in range(N)]
55 # Ignore the dynamics line if available
56 skip = 0
57 if "dynamics" in lines[7]:
58 skip += 1
59 mode = lines[7 + skip].strip().lower()
61 pos = np.empty((np.sum(Natom), 3))
62 # Following lines contain atom positions
63 for i, line in enumerate(lines[8 + skip : 8 + skip + np.sum(Natom)]):
64 if mode == "direct":
65 pos[i] = np.sum(a * np.asarray(line.strip().split()[:3], dtype=float), axis=0)
66 if mode == "cartesian":
67 pos[i] = scaling * np.asarray(line.strip().split()[:3], dtype=float)
68 # Skip all the properties afterwards
70 # POSCAR files are in Angstrom, so convert to Bohr
71 pos = xp.asarray(ang2bohr(pos))
72 a = xp.asarray(ang2bohr(a))
73 return atom, pos, a
76def write_poscar(obj, filename, fods=None, elec_symbols=("X", "He")):
77 """Generate POSCAR files from atoms objects.
79 File format definition: https://www.vasp.at/wiki/index.php/POSCAR
81 Args:
82 obj: Atoms or SCF object.
83 filename: POSCAR output file path/name.
85 Keyword Args:
86 fods: FOD coordinates to write.
87 elec_symbols: Identifier for up and down FODs.
88 """
89 atoms = obj._atoms
91 if "POSCAR" not in filename:
92 filename += ".POSCAR"
94 # Convert the coordinates from atomic units to Angstrom
95 pos = bohr2ang(atoms.pos)
96 a = bohr2ang(atoms.a)
97 if fods is not None:
98 fods = [bohr2ang(i) for i in fods]
100 if "He" in atoms.atom and atoms.unrestricted:
101 log.warning(
102 'You need to modify "elec_symbols" to write helium with FODs in the spin-'
103 "polarized case."
104 )
106 with open(filename, "w", encoding="utf-8") as fp:
107 # Print information about the file and program, and the file creation time
108 fp.write(f"File generated with eminus {__version__} on {time.ctime()}\n")
110 # We have scaled vectors and coordinates
111 fp.write("1.0\n")
113 # Write lattice
114 fp.write(
115 f"{a[0, 0]:.6f} {a[0, 1]:.6f} {a[0, 2]:.6f}\n"
116 f"{a[1, 0]:.6f} {a[1, 1]:.6f} {a[1, 2]:.6f}\n"
117 f"{a[2, 0]:.6f} {a[2, 1]:.6f} {a[2, 2]:.6f}\n"
118 )
120 # We need to sort the atoms in the POSCAR file
121 sort = np.argsort(atoms.atom)
122 atom = np.asarray(atoms.atom)[sort]
123 pos = pos[sort]
125 # Write the sorted species
126 fp.write(f"{' '.join(set(atom))}")
127 if fods is not None:
128 for s in range(len(fods)):
129 if len(fods[s]) > 0:
130 fp.write(f" {elec_symbols[s]}")
131 fp.write("\n")
133 # Write the number per (sorted) species
134 _, counts = np.unique(atom, return_counts=True)
135 fp.write(f"{' '.join(map(str, counts))}")
136 if fods is not None:
137 for fod in fods:
138 if len(fod) > 0:
139 fp.write(f" {len(fod)}")
140 fp.write("\nCartesian\n")
142 # Write the coordinates
143 fp.writelines(
144 f"{pos[ia, 0]: .6f} {pos[ia, 1]: .6f} {pos[ia, 2]: .6f}\n"
145 for ia in range(atoms.Natoms)
146 )
148 # Add FOD coordinates if needed
149 if fods is not None:
150 for s in range(len(fods)):
151 fp.writelines(f"{ie[0]: .6f} {ie[1]: .6f} {ie[2]: .6f}\n" for ie in fods[s])