Coverage for /builds/ase/ase/ase/gui/images.py : 70.42%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from math import sqrt
3import numpy as np
5from ase import Atoms
6from ase.calculators.singlepoint import SinglePointCalculator
7from ase.constraints import FixAtoms
8from ase.data import covalent_radii
9from ase.gui.defaults import read_defaults
10from ase.io import read, write, string2index
11from ase.gui.i18n import _
12from ase.geometry import find_mic
14import warnings
17class Images:
18 def __init__(self, images=None):
19 self.covalent_radii = covalent_radii.copy()
20 self.config = read_defaults()
21 self.atom_scale = self.config['radii_scale']
22 if images is None:
23 images = [Atoms()]
24 self.initialize(images)
26 def __len__(self):
27 return len(self._images)
29 def __getitem__(self, index):
30 return self._images[index]
32 def __iter__(self):
33 return iter(self._images)
35 # XXXXXXX hack
36 # compatibility hacks while allowing variable number of atoms
37 def get_dynamic(self, atoms):
38 dynamic = np.ones(len(atoms), bool)
39 for constraint in atoms.constraints:
40 if isinstance(constraint, FixAtoms):
41 dynamic[constraint.index] = False
42 return dynamic
44 def set_dynamic(self, mask, value):
45 # Does not make much sense if different images have different
46 # atom counts. Attempts to apply mask to all images,
47 # to the extent possible.
48 for atoms in self:
49 dynamic = self.get_dynamic(atoms)
50 dynamic[mask[:len(atoms)]] = value
51 atoms.constraints = [c for c in atoms.constraints
52 if not isinstance(c, FixAtoms)]
53 atoms.constraints.append(FixAtoms(mask=~dynamic))
55 def scale_radii(self, scaling_factor):
56 self.covalent_radii *= scaling_factor
58 def get_energy(self, atoms):
59 try:
60 e = atoms.get_potential_energy() * self.repeat.prod()
61 except RuntimeError:
62 e = np.nan
63 return e
65 def get_forces(self, atoms):
66 try:
67 F = atoms.get_forces(apply_constraint=False)
68 except RuntimeError:
69 return None
70 else:
71 return F
73 def initialize(self, images, filenames=None):
74 nimages = len(images)
75 if filenames is None:
76 filenames = [None] * nimages
77 self.filenames = filenames
79 warning = False
81 self._images = []
83 # Whether length or chemical composition changes:
84 self.have_varying_species = False
85 for i, atoms in enumerate(images):
86 # copy atoms or not? Not copying allows back-editing,
87 # but copying actually forgets things like the attached
88 # calculator (might have forces/energies
89 self._images.append(atoms)
90 self.have_varying_species |= not np.array_equal(self[0].numbers,
91 atoms.numbers)
92 if hasattr(self, 'Q'):
93 assert False # XXX askhl fix quaternions
94 self.Q[i] = atoms.get_quaternions()
95 if (atoms.pbc != self[0].pbc).any():
96 warning = True
98 if warning:
99 import warnings
100 warnings.warn('Not all images have the same boundary conditions!')
102 self.maxnatoms = max(len(atoms) for atoms in self)
103 self.selected = np.zeros(self.maxnatoms, bool)
104 self.selected_ordered = []
105 self.visible = np.ones(self.maxnatoms, bool)
106 self.repeat = np.ones(3, int)
108 def get_radii(self, atoms):
109 radii = np.array([self.covalent_radii[z] for z in atoms.numbers])
110 radii *= self.atom_scale
111 return radii
113 def read(self, filenames, default_index=':', filetype=None):
114 if isinstance(default_index, str):
115 default_index = string2index(default_index)
117 images = []
118 names = []
119 for filename in filenames:
120 from ase.io.formats import parse_filename
122 if '@' in filename and 'postgres' not in filename or \
123 'postgres' in filename and filename.count('@') == 2:
124 actual_filename, index = parse_filename(filename, None)
125 else:
126 actual_filename, index = parse_filename(filename,
127 default_index)
129 # Read from stdin:
130 if filename == '-':
131 import sys
132 from io import BytesIO
133 buf = BytesIO(sys.stdin.buffer.read())
134 buf.seek(0)
135 filename = buf
136 filetype = 'traj'
138 imgs = read(filename, index, filetype)
139 if hasattr(imgs, 'iterimages'):
140 imgs = list(imgs.iterimages())
142 images.extend(imgs)
144 # Name each file as filename@index:
145 if isinstance(index, slice):
146 start = index.start or 0
147 step = index.step or 1
148 else:
149 start = index
150 step = 1
151 for i, img in enumerate(imgs):
152 if isinstance(start, int):
153 names.append('{}@{}'.format(
154 actual_filename, start + i * step))
155 else:
156 names.append('{}@{}'.format(actual_filename, start))
158 self.initialize(images, names)
160 def repeat_results(self, atoms, repeat=None, oldprod=None):
161 """Return a dictionary which updates the magmoms, energy and forces
162 to the repeated amount of atoms.
163 """
164 def getresult(name, get_quantity):
165 # ase/io/trajectory.py line 170 does this by using
166 # the get_property(prop, atoms, allow_calculation=False)
167 # so that is an alternative option.
168 try:
169 if (not atoms.calc or
170 atoms.calc.calculation_required(atoms, [name])):
171 quantity = None
172 else:
173 quantity = get_quantity()
174 except Exception as err:
175 quantity = None
176 errmsg = ('An error occurred while retrieving {} '
177 'from the calculator: {}'.format(name, err))
178 warnings.warn(errmsg)
179 return quantity
181 if repeat is None:
182 repeat = self.repeat.prod()
183 if oldprod is None:
184 oldprod = self.repeat.prod()
186 results = {}
188 original_length = len(atoms) // oldprod
189 newprod = repeat.prod()
191 # Read the old properties
192 magmoms = getresult('magmoms', atoms.get_magnetic_moments)
193 magmom = getresult('magmom', atoms.get_magnetic_moment)
194 energy = getresult('energy', atoms.get_potential_energy)
195 forces = getresult('forces', atoms.get_forces)
197 # Update old properties to the repeated image
198 if magmoms is not None:
199 magmoms = np.tile(magmoms[:original_length], newprod)
200 results['magmoms'] = magmoms
202 if magmom is not None:
203 magmom = magmom * newprod / oldprod
204 results['magmom'] = magmom
206 if forces is not None:
207 forces = np.tile(forces[:original_length].T, newprod).T
208 results['forces'] = forces
210 if energy is not None:
211 energy = energy * newprod / oldprod
212 results['energy'] = energy
214 return results
216 def repeat_unit_cell(self):
217 for atoms in self:
218 # Get quantities taking into account current repeat():'
219 results = self.repeat_results(atoms, self.repeat.prod(),
220 oldprod=self.repeat.prod())
222 atoms.cell *= self.repeat.reshape((3, 1))
223 atoms.calc = SinglePointCalculator(atoms, **results)
224 self.repeat = np.ones(3, int)
226 def repeat_images(self, repeat):
227 from ase.constraints import FixAtoms
228 repeat = np.array(repeat)
229 oldprod = self.repeat.prod()
230 images = []
231 constraints_removed = False
233 for i, atoms in enumerate(self):
234 refcell = atoms.get_cell()
235 fa = []
236 for c in atoms._constraints:
237 if isinstance(c, FixAtoms):
238 fa.append(c)
239 else:
240 constraints_removed = True
241 atoms.set_constraint(fa)
243 # Update results dictionary to repeated atoms
244 results = self.repeat_results(atoms, repeat, oldprod)
246 del atoms[len(atoms) // oldprod:] # Original atoms
248 atoms *= repeat
249 atoms.cell = refcell
251 atoms.calc = SinglePointCalculator(atoms, **results)
253 images.append(atoms)
255 if constraints_removed:
256 from ase.gui.ui import tk, showwarning
257 # We must be able to show warning before the main GUI
258 # has been created. So we create a new window,
259 # then show the warning, then destroy the window.
260 tmpwindow = tk.Tk()
261 tmpwindow.withdraw() # Host window will never be shown
262 showwarning(_('Constraints discarded'),
263 _('Constraints other than FixAtoms '
264 'have been discarded.'))
265 tmpwindow.destroy()
267 self.initialize(images, filenames=self.filenames)
268 self.repeat = repeat
270 def center(self):
271 """Center each image in the existing unit cell, keeping the
272 cell constant."""
273 for atoms in self:
274 atoms.center()
276 def graph(self, expr):
277 """Routine to create the data in graphs, defined by the
278 string expr."""
279 import ase.units as units
280 code = compile(expr + ',', '<input>', 'eval')
282 nimages = len(self)
284 def d(n1, n2):
285 return sqrt(((R[n1] - R[n2])**2).sum())
287 def a(n1, n2, n3):
288 v1 = R[n1] - R[n2]
289 v2 = R[n3] - R[n2]
290 arg = np.vdot(v1, v2) / (sqrt((v1**2).sum() * (v2**2).sum()))
291 if arg > 1.0:
292 arg = 1.0
293 if arg < -1.0:
294 arg = -1.0
295 return 180.0 * np.arccos(arg) / np.pi
297 def dih(n1, n2, n3, n4):
298 # vector 0->1, 1->2, 2->3 and their normalized cross products:
299 a = R[n2] - R[n1]
300 b = R[n3] - R[n2]
301 c = R[n4] - R[n3]
302 bxa = np.cross(b, a)
303 bxa /= np.sqrt(np.vdot(bxa, bxa))
304 cxb = np.cross(c, b)
305 cxb /= np.sqrt(np.vdot(cxb, cxb))
306 angle = np.vdot(bxa, cxb)
307 # check for numerical trouble due to finite precision:
308 if angle < -1:
309 angle = -1
310 if angle > 1:
311 angle = 1
312 angle = np.arccos(angle)
313 if np.vdot(bxa, c) > 0:
314 angle = 2 * np.pi - angle
315 return angle * 180.0 / np.pi
317 # get number of mobile atoms for temperature calculation
318 E = np.array([self.get_energy(atoms) for atoms in self])
320 s = 0.0
322 # Namespace for eval:
323 ns = {'E': E,
324 'd': d, 'a': a, 'dih': dih}
326 data = []
327 for i in range(nimages):
328 ns['i'] = i
329 ns['s'] = s
330 ns['R'] = R = self[i].get_positions()
331 ns['V'] = self[i].get_velocities()
332 F = self.get_forces(self[i])
333 if F is not None:
334 ns['F'] = F
335 ns['A'] = self[i].get_cell()
336 ns['M'] = self[i].get_masses()
337 # XXX askhl verify:
338 dynamic = self.get_dynamic(self[i])
339 if F is not None:
340 ns['f'] = f = ((F * dynamic[:, None])**2).sum(1)**.5
341 ns['fmax'] = max(f)
342 ns['fave'] = f.mean()
343 ns['epot'] = epot = E[i]
344 ns['ekin'] = ekin = self[i].get_kinetic_energy()
345 ns['e'] = epot + ekin
346 ndynamic = dynamic.sum()
347 if ndynamic > 0:
348 ns['T'] = 2.0 * ekin / (3.0 * ndynamic * units.kB)
349 data = eval(code, ns)
350 if i == 0:
351 nvariables = len(data)
352 xy = np.empty((nvariables, nimages))
353 xy[:, i] = data
354 if i + 1 < nimages and not self.have_varying_species:
355 dR = find_mic(self[i + 1].positions - R, self[i].get_cell(),
356 self[i].get_pbc())[0]
357 s += sqrt((dR**2).sum())
358 return xy
360 def write(self, filename, rotations='', bbox=None,
361 **kwargs):
362 # XXX We should show the unit cell whenever there is one
363 indices = range(len(self))
364 p = filename.rfind('@')
365 if p != -1:
366 try:
367 slice = string2index(filename[p + 1:])
368 except ValueError:
369 pass
370 else:
371 indices = indices[slice]
372 filename = filename[:p]
373 if isinstance(indices, int):
374 indices = [indices]
376 images = [self.get_atoms(i) for i in indices]
377 if len(filename) > 4 and filename[-4:] in ['.eps', '.png', '.pov']:
378 write(filename, images,
379 rotation=rotations,
380 bbox=bbox, **kwargs)
381 else:
382 write(filename, images, **kwargs)
384 def get_atoms(self, frame, remove_hidden=False):
385 atoms = self[frame]
386 try:
387 E = atoms.get_potential_energy()
388 except RuntimeError:
389 E = None
390 try:
391 F = atoms.get_forces()
392 except RuntimeError:
393 F = None
395 # Remove hidden atoms if applicable
396 if remove_hidden:
397 atoms = atoms[self.visible]
398 if F is not None:
399 F = F[self.visible]
400 atoms.calc = SinglePointCalculator(atoms, energy=E, forces=F)
401 return atoms
403 def delete(self, i):
404 self.images.pop(i)
405 self.filenames.pop(i)
406 self.initialize(self.images, self.filenames)