Coverage for eminus/io/xyz.py: 97.73%
44 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: 2023 The eminus developers
2# SPDX-License-Identifier: Apache-2.0
3"""XYZ 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_xyz(filename):
16 """Load atom species and positions from XYZ files.
18 File format definition:
19 https://open-babel.readthedocs.io/en/latest/FileFormats/XYZ_cartesian_coordinates_format.html
21 Args:
22 filename: XYZ input file path/name.
24 Returns:
25 Atom species and positions.
26 """
27 if not filename.endswith(".xyz"):
28 filename += ".xyz"
30 with open(filename, encoding="utf-8") as fh:
31 lines = fh.readlines()
33 # The first line contains the number of atoms
34 Natoms = int(lines[0].strip())
36 # The second line can contain a comment, print it if available
37 comment = lines[1].strip()
38 if comment:
39 log.info(f'XYZ file comment: "{comment}"')
41 atom = []
42 pos = []
43 # Following lines contain atom positions with the format: Atom x-pos y-pos z-pos
44 for line in lines[2 : 2 + Natoms]:
45 line_split = line.strip().split()
46 atom.append(line_split[0])
47 pos.append(np.asarray(line_split[1:4], dtype=float))
49 # XYZ files are in Angstrom, so convert to Bohr
50 pos = xp.asarray(ang2bohr(np.asarray(pos)))
51 return atom, pos
54def write_xyz(obj, filename, fods=None, elec_symbols=("X", "He"), trajectory=False):
55 """Generate XYZ files from atoms objects.
57 File format definition:
58 https://open-babel.readthedocs.io/en/latest/FileFormats/XYZ_cartesian_coordinates_format.html
60 Args:
61 obj: Atoms or SCF object.
62 filename: XYZ output file path/name.
64 Keyword Args:
65 fods: FOD coordinates to write.
66 elec_symbols: Identifier for up and down FODs.
67 trajectory: Allow appending to a file to create trajectories.
68 """
69 atoms = obj._atoms
71 # The trajectory write calls this function
72 if not filename.endswith((".xyz", ".trj", ".traj")):
73 filename += ".xyz"
75 # Convert the coordinates from atomic units to Angstrom
76 pos = bohr2ang(atoms.pos)
77 if fods is not None:
78 fods = [bohr2ang(i) for i in fods]
80 if "He" in atoms.atom and atoms.unrestricted:
81 log.warning(
82 'You need to modify "elec_symbols" to write helium with FODs in the spin-'
83 "polarized case."
84 )
86 # Append to a file when using the trajectory keyword
87 if trajectory:
88 mode = "a"
89 else:
90 mode = "w"
92 with open(filename, mode, encoding="utf-8") as fp:
93 # The first line contains the number of atoms
94 # If we add FOD coordinates, add them to the count
95 if fods is None:
96 fp.write(f"{atoms.Natoms}\n")
97 else:
98 fp.write(f"{atoms.Natoms + sum(len(i) for i in fods)}\n")
99 # The second line can contain a comment
100 # Print information about the file and program, and the file creation time
101 fp.write(f"File generated with eminus {__version__} on {time.ctime()}\n")
102 fp.writelines(
103 f"{atoms.atom[ia]:<2s} {pos[ia, 0]: .6f} {pos[ia, 1]: .6f} {pos[ia, 2]: .6f}\n"
104 for ia in range(atoms.Natoms)
105 )
106 # Add FOD coordinates if needed
107 # The atom symbol will default to pos (no atom type)
108 if fods is not None:
109 for s in range(len(fods)):
110 fp.writelines(
111 f"{elec_symbols[s]:<2s} {ie[0]: .6f} {ie[1]: .6f} {ie[2]: .6f}\n"
112 for ie in fods[s]
113 )