Coverage for /builds/kinetik161/ase/ase/io/mustem.py: 95.41%
109 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1"""Module to read and write atoms in xtl file format for the muSTEM software.
3See http://tcmp.ph.unimelb.edu.au/mustem/muSTEM.html for a few examples of
4this format and the documentation of muSTEM.
6See https://github.com/HamishGBrown/MuSTEM for the source code of muSTEM.
7"""
9import numpy as np
11from ase.atoms import Atoms, symbols2numbers
12from ase.data import chemical_symbols
13from ase.utils import reader, writer
15from .utils import verify_cell_for_export, verify_dictionary
18@reader
19def read_mustem(fd):
20 r"""Import muSTEM input file.
22 Reads cell, atom positions, etc. from muSTEM xtl file.
23 The mustem xtl save the root mean square (RMS) displacement, which is
24 convert to Debye-Waller (in Ų) factor by:
26 .. math::
28 B = RMS * 8\pi^2
30 """
31 from ase.geometry import cellpar_to_cell
33 # Read comment:
34 fd.readline()
36 # Parse unit cell parameter
37 cellpar = [float(i) for i in fd.readline().split()[:3]]
38 cell = cellpar_to_cell(cellpar)
40 # beam energy
41 fd.readline()
43 # Number of different type of atoms
44 element_number = int(fd.readline().strip())
46 # List of numpy arrays:
47 # length of the list = number of different atom type (element_number)
48 # length of each array = number of atoms for each atom type
49 atomic_numbers = []
50 positions = []
51 debye_waller_factors = []
52 occupancies = []
54 for i in range(element_number):
55 # Read the element
56 _ = fd.readline()
57 line = fd.readline().split()
58 atoms_number = int(line[0])
59 atomic_number = int(line[1])
60 occupancy = float(line[2])
61 DW = float(line[3]) * 8 * np.pi**2
62 # read all the position for each element
63 positions.append(np.genfromtxt(fname=fd, max_rows=atoms_number))
64 atomic_numbers.append(np.ones(atoms_number, dtype=int) * atomic_number)
65 occupancies.append(np.ones(atoms_number) * occupancy)
66 debye_waller_factors.append(np.ones(atoms_number) * DW)
68 positions = np.vstack(positions)
70 atoms = Atoms(cell=cell, scaled_positions=positions)
71 atoms.set_atomic_numbers(np.hstack(atomic_numbers))
72 atoms.set_array('occupancies', np.hstack(occupancies))
73 atoms.set_array('debye_waller_factors', np.hstack(debye_waller_factors))
75 return atoms
78class XtlmuSTEMWriter:
79 """See the docstring of the `write_mustem` function.
80 """
82 def __init__(self, atoms, keV, debye_waller_factors=None,
83 comment=None, occupancies=None, fit_cell_to_atoms=False):
84 verify_cell_for_export(atoms.get_cell())
86 self.atoms = atoms.copy()
87 self.atom_types = sorted(set(atoms.symbols))
88 self.keV = keV
89 self.comment = comment
90 self.occupancies = self._get_occupancies(occupancies)
91 self.RMS = self._get_RMS(debye_waller_factors)
93 self.numbers = symbols2numbers(self.atom_types)
94 if fit_cell_to_atoms:
95 self.atoms.translate(-self.atoms.positions.min(axis=0))
96 self.atoms.set_cell(self.atoms.positions.max(axis=0))
98 def _get_occupancies(self, occupancies):
99 if occupancies is None:
100 if 'occupancies' in self.atoms.arrays:
101 occupancies = {element:
102 self._parse_array_from_atoms(
103 'occupancies', element, True)
104 for element in self.atom_types}
105 else:
106 occupancies = 1.0
107 if np.isscalar(occupancies):
108 occupancies = {atom: occupancies for atom in self.atom_types}
109 elif isinstance(occupancies, dict):
110 verify_dictionary(self.atoms, occupancies, 'occupancies')
112 return occupancies
114 def _get_RMS(self, DW):
115 if DW is None:
116 if 'debye_waller_factors' in self.atoms.arrays:
117 DW = {element: self._parse_array_from_atoms(
118 'debye_waller_factors', element, True) / (8 * np.pi**2)
119 for element in self.atom_types}
120 elif np.isscalar(DW):
121 if len(self.atom_types) > 1:
122 raise ValueError('This cell contains more then one type of '
123 'atoms and the Debye-Waller factor needs to '
124 'be provided for each atom using a '
125 'dictionary.')
126 DW = {self.atom_types[0]: DW / (8 * np.pi**2)}
127 elif isinstance(DW, dict):
128 verify_dictionary(self.atoms, DW, 'debye_waller_factors')
129 for key, value in DW.items():
130 DW[key] = value / (8 * np.pi**2)
132 if DW is None:
133 raise ValueError('Missing Debye-Waller factors. It can be '
134 'provided as a dictionary with symbols as key or '
135 'if the cell contains only a single type of '
136 'element, the Debye-Waller factor can also be '
137 'provided as float.')
139 return DW
141 def _parse_array_from_atoms(self, name, element, check_same_value):
142 """
143 Return the array "name" for the given element.
145 Parameters
146 ----------
147 name : str
148 The name of the arrays. Can be any key of `atoms.arrays`
149 element : str, int
150 The element to be considered.
151 check_same_value : bool
152 Check if all values are the same in the array. Necessary for
153 'occupancies' and 'debye_waller_factors' arrays.
155 Returns
156 -------
157 array containing the values corresponding defined by "name" for the
158 given element. If check_same_value, return a single element.
160 """
161 if isinstance(element, str):
162 element = symbols2numbers(element)[0]
163 sliced_array = self.atoms.arrays[name][self.atoms.numbers == element]
165 if check_same_value:
166 # to write the occupancies and the Debye Waller factor of xtl file
167 # all the value must be equal
168 if np.unique(sliced_array).size > 1:
169 raise ValueError(
170 "All the '{}' values for element '{}' must be "
171 "equal.".format(name, chemical_symbols[element])
172 )
173 sliced_array = sliced_array[0]
175 return sliced_array
177 def _get_position_array_single_atom_type(self, number):
178 # Get the scaled (reduced) position for a single atom type
179 return self.atoms.get_scaled_positions()[
180 self.atoms.numbers == number]
182 def _get_file_header(self):
183 # 1st line: comment line
184 if self.comment is None:
185 s = "{} atoms with chemical formula: {}\n".format(
186 len(self.atoms),
187 self.atoms.get_chemical_formula())
188 else:
189 s = self.comment
190 # 2nd line: lattice parameter
191 s += "{} {} {} {} {} {}\n".format(
192 *self.atoms.cell.cellpar().tolist())
193 # 3td line: acceleration voltage
194 s += f"{self.keV}\n"
195 # 4th line: number of different atom
196 s += f"{len(self.atom_types)}\n"
197 return s
199 def _get_element_header(self, atom_type, number, atom_type_number,
200 occupancy, RMS):
201 return "{}\n{} {} {} {:.3g}\n".format(atom_type,
202 number,
203 atom_type_number,
204 occupancy,
205 RMS)
207 def _get_file_end(self):
208 return "Orientation\n 1 0 0\n 0 1 0\n 0 0 1\n"
210 def write_to_file(self, fd):
211 if isinstance(fd, str):
212 fd = open(fd, 'w')
214 fd.write(self._get_file_header())
215 for atom_type, number, occupancy in zip(self.atom_types,
216 self.numbers,
217 self.occupancies):
218 positions = self._get_position_array_single_atom_type(number)
219 atom_type_number = positions.shape[0]
220 fd.write(self._get_element_header(atom_type, atom_type_number,
221 number,
222 self.occupancies[atom_type],
223 self.RMS[atom_type]))
224 np.savetxt(fname=fd, X=positions, fmt='%.6g', newline='\n')
226 fd.write(self._get_file_end())
229@writer
230def write_mustem(fd, *args, **kwargs):
231 r"""Write muSTEM input file.
233 Parameters:
235 atoms: Atoms object
237 keV: float
238 Energy of the electron beam in keV required for the image simulation.
240 debye_waller_factors: float or dictionary of float with atom type as key
241 Debye-Waller factor of each atoms. Since the prismatic/computem
242 software use root means square RMS) displacements, the Debye-Waller
243 factors (B) needs to be provided in Ų and these values are converted
244 to RMS displacement by:
246 .. math::
248 RMS = \frac{B}{8\pi^2}
250 occupancies: float or dictionary of float with atom type as key (optional)
251 Occupancy of each atoms. Default value is `1.0`.
253 comment: str (optional)
254 Comments to be written in the first line of the file. If not
255 provided, write the total number of atoms and the chemical formula.
257 fit_cell_to_atoms: bool (optional)
258 If `True`, fit the cell to the atoms positions. If negative coordinates
259 are present in the cell, the atoms are translated, so that all
260 positions are positive. If `False` (default), the atoms positions and
261 the cell are unchanged.
262 """
263 writer = XtlmuSTEMWriter(*args, **kwargs)
264 writer.write_to_file(fd)