Coverage for /builds/ase/ase/ase/io/xsf.py: 96.32%
190 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3import numpy as np
5from ase.atoms import Atoms
6from ase.calculators.singlepoint import SinglePointCalculator
7from ase.data import atomic_numbers
8from ase.units import Hartree
9from ase.utils import reader, writer
12@writer
13def write_xsf(fileobj, images, data=None, origin=None, span_vectors=None):
14 is_anim = len(images) > 1
16 if is_anim:
17 fileobj.write('ANIMSTEPS %d\n' % len(images))
19 numbers = images[0].get_atomic_numbers()
21 pbc = images[0].get_pbc()
22 npbc = sum(pbc)
23 if pbc[2]:
24 fileobj.write('CRYSTAL\n')
25 assert npbc == 3
26 elif pbc[1]:
27 fileobj.write('SLAB\n')
28 assert npbc == 2
29 elif pbc[0]:
30 fileobj.write('POLYMER\n')
31 assert npbc == 1
32 else:
33 # (Header written as part of image loop)
34 assert npbc == 0
36 cell_variable = False
37 for image in images[1:]:
38 if np.abs(images[0].cell - image.cell).max() > 1e-14:
39 cell_variable = True
40 break
42 for n, atoms in enumerate(images):
43 anim_token = ' %d' % (n + 1) if is_anim else ''
44 if pbc.any():
45 write_cell = (n == 0 or cell_variable)
46 if write_cell:
47 if cell_variable:
48 fileobj.write(f'PRIMVEC{anim_token}\n')
49 else:
50 fileobj.write('PRIMVEC\n')
51 cell = atoms.get_cell()
52 for i in range(3):
53 fileobj.write(' %.14f %.14f %.14f\n' % tuple(cell[i]))
55 fileobj.write(f'PRIMCOORD{anim_token}\n')
56 else:
57 fileobj.write(f'ATOMS{anim_token}\n')
59 # Get the forces if it's not too expensive:
60 calc = atoms.calc
61 if (calc is not None and
62 (hasattr(calc, 'calculation_required') and
63 not calc.calculation_required(atoms, ['forces']))):
64 forces = atoms.get_forces() / Hartree
65 else:
66 forces = None
68 pos = atoms.get_positions()
70 if pbc.any():
71 fileobj.write(' %d 1\n' % len(pos))
72 for a in range(len(pos)):
73 fileobj.write(' %2d' % numbers[a])
74 fileobj.write(' %20.14f %20.14f %20.14f' % tuple(pos[a]))
75 if forces is None:
76 fileobj.write('\n')
77 else:
78 fileobj.write(' %20.14f %20.14f %20.14f\n' % tuple(forces[a]))
80 if data is None:
81 return
83 fileobj.write('BEGIN_BLOCK_DATAGRID_3D\n')
84 fileobj.write(' data\n')
85 fileobj.write(' BEGIN_DATAGRID_3Dgrid#1\n')
87 data = np.asarray(data)
88 if data.dtype == complex:
89 data = np.abs(data)
91 shape = data.shape
92 fileobj.write(' %d %d %d\n' % shape)
94 cell = atoms.get_cell()
95 if origin is None:
96 origin = np.zeros(3)
97 for i in range(3):
98 if not pbc[i]:
99 origin += cell[i] / shape[i]
100 fileobj.write(' %f %f %f\n' % tuple(origin))
102 for i in range(3):
103 # XXXX is this not just supposed to be the cell?
104 # What's with the strange division?
105 # This disagrees with the output of Octopus. Investigate
106 if span_vectors is None:
107 fileobj.write(' %f %f %f\n' %
108 tuple(cell[i] * (shape[i] + 1) / shape[i]))
109 else:
110 fileobj.write(' %f %f %f\n' % tuple(span_vectors[i]))
112 for k in range(shape[2]):
113 for j in range(shape[1]):
114 fileobj.write(' ')
115 fileobj.write(' '.join(['%f' % d for d in data[:, j, k]]))
116 fileobj.write('\n')
117 fileobj.write('\n')
119 fileobj.write(' END_DATAGRID_3D\n')
120 fileobj.write('END_BLOCK_DATAGRID_3D\n')
123@reader
124def iread_xsf(fileobj, read_data=False):
125 """Yield images and optionally data from xsf file.
127 Yields image1, image2, ..., imageN[, data, origin,
128 span_vectors].
130 Images are Atoms objects and data is a numpy array.
132 It also returns the origin of the simulation box
133 as a numpy array and its spanning vectors as a
134 list of numpy arrays, if data is returned.
136 Presently supports only a single 3D datagrid."""
137 def _line_generator_func():
138 for line in fileobj:
139 line = line.strip()
140 if not line or line.startswith('#'):
141 continue # Discard comments and empty lines
142 yield line
144 _line_generator = _line_generator_func()
146 def readline():
147 return next(_line_generator)
149 line = readline()
151 if line.startswith('ANIMSTEPS'):
152 nimages = int(line.split()[1])
153 line = readline()
154 else:
155 nimages = 1
157 if line == 'CRYSTAL':
158 pbc = (True, True, True)
159 elif line == 'SLAB':
160 pbc = (True, True, False)
161 elif line == 'POLYMER':
162 pbc = (True, False, False)
163 else:
164 assert line.startswith('ATOMS'), line # can also be ATOMS 1
165 pbc = (False, False, False)
167 cell = None
168 for n in range(nimages):
169 if any(pbc):
170 line = readline()
171 if line.startswith('PRIMCOORD'):
172 assert cell is not None # cell read from previous image
173 else:
174 assert line.startswith('PRIMVEC')
175 cell = []
176 for i in range(3):
177 cell.append([float(x) for x in readline().split()])
179 line = readline()
180 if line.startswith('CONVVEC'): # ignored;
181 for i in range(3):
182 readline()
183 line = readline()
185 assert line.startswith('PRIMCOORD')
186 natoms = int(readline().split()[0])
187 lines = [readline() for _ in range(natoms)]
188 else:
189 assert line.startswith('ATOMS'), line
190 line = readline()
191 lines = []
192 while not (line.startswith('ATOMS') or line.startswith('BEGIN')):
193 lines.append(line)
194 try:
195 line = readline()
196 except StopIteration:
197 break
198 if line.startswith('BEGIN'):
199 # We read "too far" and accidentally got the header
200 # of the data section. This happens only when parsing
201 # ATOMS blocks, because one cannot infer their length.
202 # We will remember the line until later then.
203 data_header_line = line
205 numbers = []
206 positions = []
207 for positionline in lines:
208 tokens = positionline.split()
209 symbol = tokens[0]
210 if symbol.isdigit():
211 numbers.append(int(symbol))
212 else:
213 numbers.append(atomic_numbers[symbol.capitalize()])
214 positions.append([float(x) for x in tokens[1:]])
216 positions = np.array(positions)
217 if len(positions[0]) == 3:
218 forces = None
219 else:
220 forces = positions[:, 3:] * Hartree
221 positions = positions[:, :3]
223 image = Atoms(numbers, positions, cell=cell, pbc=pbc)
225 if forces is not None:
226 image.calc = SinglePointCalculator(image, forces=forces)
227 yield image
229 if read_data:
230 if any(pbc):
231 line = readline()
232 else:
233 line = data_header_line
234 assert line.startswith('BEGIN_BLOCK_DATAGRID_3D')
235 readline() # name
236 line = readline()
237 assert line.startswith('BEGIN_DATAGRID_3D')
239 shape = [int(x) for x in readline().split()]
240 assert len(shape) == 3
241 origin = [float(x) for x in readline().split()]
242 origin = np.array(origin)
244 span_vectors = []
245 for i in range(3):
246 span_vector = [float(x) for x in readline().split()]
247 span_vector = np.array(span_vector)
248 span_vectors.append(span_vector)
249 span_vectors = np.array(span_vectors)
250 assert len(span_vectors) == len(shape)
252 npoints = np.prod(shape)
254 data = []
255 line = readline() # First line of data
256 while not line.startswith('END_DATAGRID_3D'):
257 data.extend([float(x) for x in line.split()])
258 line = readline()
259 assert len(data) == npoints
260 data = np.array(data, float).reshape(shape[::-1]).T
261 # Note that data array is Fortran-ordered
262 yield data, origin, span_vectors
265def read_xsf(fileobj, index=-1, read_data=False):
266 images = list(iread_xsf(fileobj, read_data=read_data))
267 if read_data:
268 array, origin, span_vectors = images[-1]
269 images = images[:-1]
270 return array, origin, span_vectors, images[index]
271 return images[index]