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 cos, sin, sqrt
2from os.path import basename
4import numpy as np
6from ase.calculators.calculator import PropertyNotImplementedError
7from ase.data import atomic_numbers
8from ase.data.colors import jmol_colors
9from ase.geometry import complete_cell
10from ase.gui.repeat import Repeat
11from ase.gui.rotate import Rotate
12from ase.gui.render import Render
13from ase.gui.colors import ColorWindow
14from ase.gui.utils import get_magmoms
15from ase.utils import rotate
17GREEN = '#74DF00'
18PURPLE = '#AC58FA'
19BLACKISH = '#151515'
22def get_cell_coordinates(cell, shifted=False):
23 """Get start and end points of lines segments used to draw cell."""
24 nn = []
25 for c in range(3):
26 v = cell[c]
27 d = sqrt(np.dot(v, v))
28 if d < 1e-12:
29 n = 0
30 else:
31 n = max(2, int(d / 0.3))
32 nn.append(n)
33 B1 = np.zeros((2, 2, sum(nn), 3))
34 B2 = np.zeros((2, 2, sum(nn), 3))
35 n1 = 0
36 for c, n in enumerate(nn):
37 n2 = n1 + n
38 h = 1.0 / (2 * n - 1)
39 R = np.arange(n) * (2 * h)
41 for i, j in [(0, 0), (0, 1), (1, 0), (1, 1)]:
42 B1[i, j, n1:n2, c] = R
43 B1[i, j, n1:n2, (c + 1) % 3] = i
44 B1[i, j, n1:n2, (c + 2) % 3] = j
45 B2[:, :, n1:n2] = B1[:, :, n1:n2]
46 B2[:, :, n1:n2, c] += h
47 n1 = n2
48 B1.shape = (-1, 3)
49 B2.shape = (-1, 3)
50 if shifted:
51 B1 -= 0.5
52 B2 -= 0.5
53 return B1, B2
56def get_bonds(atoms, covalent_radii):
57 from ase.neighborlist import NeighborList
58 nl = NeighborList(covalent_radii * 1.5,
59 skin=0, self_interaction=False)
60 nl.update(atoms)
61 nbonds = nl.nneighbors + nl.npbcneighbors
63 bonds = np.empty((nbonds, 5), int)
64 if nbonds == 0:
65 return bonds
67 n1 = 0
68 for a in range(len(atoms)):
69 indices, offsets = nl.get_neighbors(a)
70 n2 = n1 + len(indices)
71 bonds[n1:n2, 0] = a
72 bonds[n1:n2, 1] = indices
73 bonds[n1:n2, 2:] = offsets
74 n1 = n2
76 i = bonds[:n2, 2:].any(1)
77 pbcbonds = bonds[:n2][i]
78 bonds[n2:, 0] = pbcbonds[:, 1]
79 bonds[n2:, 1] = pbcbonds[:, 0]
80 bonds[n2:, 2:] = -pbcbonds[:, 2:]
81 return bonds
84class View:
85 def __init__(self, rotations):
86 self.colormode = 'jmol' # The default colors
87 self.labels = None
88 self.axes = rotate(rotations)
89 self.configured = False
90 self.frame = None
92 # XXX
93 self.colormode = 'jmol'
94 self.colors = {}
96 for i, rgb in enumerate(jmol_colors):
97 self.colors[i] = ('#{0:02X}{1:02X}{2:02X}'
98 .format(*(int(x * 255) for x in rgb)))
100 # scaling factors for vectors
101 self.force_vector_scale = self.config['force_vector_scale']
102 self.velocity_vector_scale = self.config['velocity_vector_scale']
104 # buttons
105 self.b1 = 1 # left
106 self.b3 = 3 # right
107 if self.config['swap_mouse']:
108 self.b1 = 3
109 self.b3 = 1
111 @property
112 def atoms(self):
113 return self.images[self.frame]
115 def set_frame(self, frame=None, focus=False):
116 if frame is None:
117 frame = self.frame
118 assert frame < len(self.images)
119 self.frame = frame
120 self.set_atoms(self.images[frame])
122 fname = self.images.filenames[frame]
123 if fname is None:
124 title = 'ase.gui'
125 else:
126 title = basename(fname)
128 self.window.title = title
130 self.call_observers()
132 if focus:
133 self.focus()
134 else:
135 self.draw()
137 def set_atoms(self, atoms):
138 natoms = len(atoms)
140 if self.showing_cell():
141 B1, B2 = get_cell_coordinates(atoms.cell,
142 self.config['shift_cell'])
143 else:
144 B1 = B2 = np.zeros((0, 3))
146 if self.showing_bonds():
147 atomscopy = atoms.copy()
148 atomscopy.cell *= self.images.repeat[:, np.newaxis]
149 bonds = get_bonds(atomscopy, self.get_covalent_radii(atoms))
150 else:
151 bonds = np.empty((0, 5), int)
153 # X is all atomic coordinates, and starting points of vectors
154 # like bonds and cell segments.
155 # The reason to have them all in one big list is that we like to
156 # eventually rotate/sort it by Z-order when rendering.
158 # Also B are the end points of line segments.
160 self.X = np.empty((natoms + len(B1) + len(bonds), 3))
161 self.X_pos = self.X[:natoms]
162 self.X_pos[:] = atoms.positions
163 self.X_cell = self.X[natoms:natoms + len(B1)]
164 self.X_bonds = self.X[natoms + len(B1):]
166 if 1: # if init or frame != self.frame:
167 cell = atoms.cell
168 ncellparts = len(B1)
169 nbonds = len(bonds)
171 if 1: # init or (atoms.cell != self.atoms.cell).any():
172 self.X_cell[:] = np.dot(B1, cell)
173 self.B = np.empty((ncellparts + nbonds, 3))
174 self.B[:ncellparts] = np.dot(B2, cell)
176 if nbonds > 0:
177 P = atoms.positions
178 Af = self.images.repeat[:, np.newaxis] * cell
179 a = P[bonds[:, 0]]
180 b = P[bonds[:, 1]] + np.dot(bonds[:, 2:], Af) - a
181 d = (b**2).sum(1)**0.5
182 r = 0.65 * self.get_covalent_radii()
183 x0 = (r[bonds[:, 0]] / d).reshape((-1, 1))
184 x1 = (r[bonds[:, 1]] / d).reshape((-1, 1))
185 self.X_bonds[:] = a + b * x0
186 b *= 1.0 - x0 - x1
187 b[bonds[:, 2:].any(1)] *= 0.5
188 self.B[ncellparts:] = self.X_bonds + b
190 def showing_bonds(self):
191 return self.window['toggle-show-bonds']
193 def showing_cell(self):
194 return self.window['toggle-show-unit-cell']
196 def toggle_show_unit_cell(self, key=None):
197 self.set_frame()
199 def update_labels(self):
200 index = self.window['show-labels']
201 if index == 0:
202 self.labels = None
203 elif index == 1:
204 self.labels = list(range(len(self.atoms)))
205 elif index == 2:
206 self.labels = list(get_magmoms(self.atoms))
207 elif index == 4:
208 Q = self.atoms.get_initial_charges()
209 self.labels = ['{0:.4g}'.format(q) for q in Q]
210 else:
211 self.labels = self.atoms.get_chemical_symbols()
213 def show_labels(self):
214 self.update_labels()
215 self.draw()
217 def toggle_show_axes(self, key=None):
218 self.draw()
220 def toggle_show_bonds(self, key=None):
221 self.set_frame()
223 def toggle_show_velocities(self, key=None):
224 self.draw()
226 def get_forces(self):
227 if self.atoms.calc is not None:
228 try:
229 return self.atoms.get_forces()
230 except PropertyNotImplementedError:
231 pass
232 return np.zeros((len(self.atoms), 3))
234 def toggle_show_forces(self, key=None):
235 self.draw()
237 def hide_selected(self):
238 self.images.visible[self.images.selected] = False
239 self.draw()
241 def show_selected(self):
242 self.images.visible[self.images.selected] = True
243 self.draw()
245 def repeat_window(self, key=None):
246 return Repeat(self)
248 def rotate_window(self):
249 return Rotate(self)
251 def colors_window(self, key=None):
252 win = ColorWindow(self)
253 self.register_vulnerable(win)
254 return win
256 def focus(self, x=None):
257 cell = (self.window['toggle-show-unit-cell'] and
258 self.images[0].cell.any())
259 if (len(self.atoms) == 0 and not cell):
260 self.scale = 20.0
261 self.center = np.zeros(3)
262 self.draw()
263 return
265 # Get the min and max point of the projected atom positions
266 # including the covalent_radii used for drawing the atoms
267 P = np.dot(self.X, self.axes)
268 n = len(self.atoms)
269 covalent_radii = self.get_covalent_radii()
270 P[:n] -= covalent_radii[:, None]
271 P1 = P.min(0)
272 P[:n] += 2 * covalent_radii[:, None]
273 P2 = P.max(0)
274 self.center = np.dot(self.axes, (P1 + P2) / 2)
275 self.center += self.atoms.get_celldisp().reshape((3,)) / 2
276 # Add 30% of whitespace on each side of the atoms
277 S = 1.3 * (P2 - P1)
278 w, h = self.window.size
279 if S[0] * h < S[1] * w:
280 self.scale = h / S[1]
281 elif S[0] > 0.0001:
282 self.scale = w / S[0]
283 else:
284 self.scale = 1.0
285 self.draw()
287 def reset_view(self, menuitem):
288 self.axes = rotate('0.0x,0.0y,0.0z')
289 self.set_frame()
290 self.focus(self)
292 def set_view(self, key):
293 if key == 'Z':
294 self.axes = rotate('0.0x,0.0y,0.0z')
295 elif key == 'X':
296 self.axes = rotate('-90.0x,-90.0y,0.0z')
297 elif key == 'Y':
298 self.axes = rotate('90.0x,0.0y,90.0z')
299 elif key == 'Alt+Z':
300 self.axes = rotate('180.0x,0.0y,90.0z')
301 elif key == 'Alt+X':
302 self.axes = rotate('0.0x,90.0y,0.0z')
303 elif key == 'Alt+Y':
304 self.axes = rotate('-90.0x,0.0y,0.0z')
305 else:
306 if key == '3':
307 i, j = 0, 1
308 elif key == '1':
309 i, j = 1, 2
310 elif key == '2':
311 i, j = 2, 0
312 elif key == 'Alt+3':
313 i, j = 1, 0
314 elif key == 'Alt+1':
315 i, j = 2, 1
316 elif key == 'Alt+2':
317 i, j = 0, 2
319 A = complete_cell(self.atoms.cell)
320 x1 = A[i]
321 x2 = A[j]
323 norm = np.linalg.norm
325 x1 = x1 / norm(x1)
326 x2 = x2 - x1 * np.dot(x1, x2)
327 x2 /= norm(x2)
328 x3 = np.cross(x1, x2)
330 self.axes = np.array([x1, x2, x3]).T
332 self.set_frame()
334 def get_colors(self, rgb=False):
335 if rgb:
336 return [tuple(int(_rgb[i:i + 2], 16) / 255 for i in range(1, 7, 2))
337 for _rgb in self.get_colors()]
339 if self.colormode == 'jmol':
340 return [self.colors.get(Z, BLACKISH) for Z in self.atoms.numbers]
342 if self.colormode == 'neighbors':
343 return [self.colors.get(Z, BLACKISH)
344 for Z in self.get_color_scalars()]
346 colorscale, cmin, cmax = self.colormode_data
347 N = len(colorscale)
348 colorswhite = colorscale + ['#ffffff']
349 if cmin == cmax:
350 indices = [N // 2] * len(self.atoms)
351 else:
352 scalars = np.ma.array(self.get_color_scalars())
353 indices = np.clip(((scalars - cmin) / (cmax - cmin) * N +
354 0.5).astype(int),
355 0, N - 1)
356 return [colorswhite[i] for i in indices.filled(N)]
358 def get_color_scalars(self, frame=None):
359 if self.colormode == 'tag':
360 return self.atoms.get_tags()
361 if self.colormode == 'force':
362 f = (self.get_forces()**2).sum(1)**0.5
363 return f * self.images.get_dynamic(self.atoms)
364 elif self.colormode == 'velocity':
365 return (self.atoms.get_velocities()**2).sum(1)**0.5
366 elif self.colormode == 'initial charge':
367 return self.atoms.get_initial_charges()
368 elif self.colormode == 'magmom':
369 return get_magmoms(self.atoms)
370 elif self.colormode == 'neighbors':
371 from ase.neighborlist import NeighborList
372 n = len(self.atoms)
373 nl = NeighborList(self.get_covalent_radii(self.atoms) * 1.5,
374 skin=0, self_interaction=False, bothways=True)
375 nl.update(self.atoms)
376 return [len(nl.get_neighbors(i)[0]) for i in range(n)]
377 else:
378 scalars = np.array(self.atoms.get_array(self.colormode),
379 dtype=float)
380 return np.ma.array(scalars, mask=np.isnan(scalars))
382 def get_covalent_radii(self, atoms=None):
383 if atoms is None:
384 atoms = self.atoms
385 return self.images.get_radii(atoms)
387 def draw(self, status=True):
388 self.window.clear()
389 axes = self.scale * self.axes * (1, -1, 1)
390 offset = np.dot(self.center, axes)
391 offset[:2] -= 0.5 * self.window.size
392 X = np.dot(self.X, axes) - offset
393 n = len(self.atoms)
395 # The indices enumerate drawable objects in z order:
396 self.indices = X[:, 2].argsort()
397 r = self.get_covalent_radii() * self.scale
398 if self.window['toggle-show-bonds']:
399 r *= 0.65
400 P = self.P = X[:n, :2]
401 A = (P - r[:, None]).round().astype(int)
402 X1 = X[n:, :2].round().astype(int)
403 X2 = (np.dot(self.B, axes) - offset).round().astype(int)
404 disp = (np.dot(self.atoms.get_celldisp().reshape((3,)),
405 axes)).round().astype(int)
406 d = (2 * r).round().astype(int)
408 vector_arrays = []
409 if self.window['toggle-show-velocities']:
410 # Scale ugly?
411 v = self.atoms.get_velocities()
412 if v is not None:
413 vector_arrays.append(v * 10.0 * self.velocity_vector_scale)
414 if self.window['toggle-show-forces']:
415 f = self.get_forces()
416 vector_arrays.append(f * self.force_vector_scale)
418 for array in vector_arrays:
419 array[:] = np.dot(array, axes) + X[:n]
421 colors = self.get_colors()
422 circle = self.window.circle
423 arc = self.window.arc
424 line = self.window.line
425 constrained = ~self.images.get_dynamic(self.atoms)
427 selected = self.images.selected
428 visible = self.images.visible
429 ncell = len(self.X_cell)
430 bond_linewidth = self.scale * 0.15
432 self.update_labels()
434 if self.arrowkey_mode == self.ARROWKEY_MOVE:
435 movecolor = GREEN
436 elif self.arrowkey_mode == self.ARROWKEY_ROTATE:
437 movecolor = PURPLE
439 for a in self.indices:
440 if a < n:
441 ra = d[a]
442 if visible[a]:
443 try:
444 kinds = self.atoms.arrays['spacegroup_kinds']
445 site_occ = self.atoms.info['occupancy'][str(kinds[a])]
446 # first an empty circle if a site is not fully occupied
447 if (np.sum([v for v in site_occ.values()])) < 1.0:
448 fill = '#ffffff'
449 circle(fill, selected[a],
450 A[a, 0], A[a, 1],
451 A[a, 0] + ra, A[a, 1] + ra)
452 start = 0
453 # start with the dominant species
454 for sym, occ in sorted(site_occ.items(),
455 key=lambda x: x[1],
456 reverse=True):
457 if np.round(occ, decimals=4) == 1.0:
458 circle(colors[a], selected[a],
459 A[a, 0], A[a, 1],
460 A[a, 0] + ra, A[a, 1] + ra)
461 else:
462 # jmol colors for the moment
463 extent = 360. * occ
464 arc(self.colors[atomic_numbers[sym]],
465 selected[a],
466 start, extent,
467 A[a, 0], A[a, 1],
468 A[a, 0] + ra, A[a, 1] + ra)
469 start += extent
470 except KeyError:
471 # legacy behavior
472 # Draw the atoms
473 if (self.moving and a < len(self.move_atoms_mask)
474 and self.move_atoms_mask[a]):
475 circle(movecolor, False,
476 A[a, 0] - 4, A[a, 1] - 4,
477 A[a, 0] + ra + 4, A[a, 1] + ra + 4)
479 circle(colors[a], selected[a],
480 A[a, 0], A[a, 1], A[a, 0] + ra, A[a, 1] + ra)
482 # Draw labels on the atoms
483 if self.labels is not None:
484 self.window.text(A[a, 0] + ra / 2,
485 A[a, 1] + ra / 2,
486 str(self.labels[a]))
488 # Draw cross on constrained atoms
489 if constrained[a]:
490 R1 = int(0.14644 * ra)
491 R2 = int(0.85355 * ra)
492 line((A[a, 0] + R1, A[a, 1] + R1,
493 A[a, 0] + R2, A[a, 1] + R2))
494 line((A[a, 0] + R2, A[a, 1] + R1,
495 A[a, 0] + R1, A[a, 1] + R2))
497 # Draw velocities and/or forces
498 for v in vector_arrays:
499 assert not np.isnan(v).any()
500 self.arrow((X[a, 0], X[a, 1], v[a, 0], v[a, 1]),
501 width=2)
502 else:
503 # Draw unit cell and/or bonds:
504 a -= n
505 if a < ncell:
506 line((X1[a, 0] + disp[0], X1[a, 1] + disp[1],
507 X2[a, 0] + disp[0], X2[a, 1] + disp[1]))
508 else:
509 line((X1[a, 0], X1[a, 1],
510 X2[a, 0], X2[a, 1]),
511 width=bond_linewidth)
513 if self.window['toggle-show-axes']:
514 self.draw_axes()
516 if len(self.images) > 1:
517 self.draw_frame_number()
519 self.window.update()
521 if status:
522 self.status(self.atoms)
524 def arrow(self, coords, width):
525 line = self.window.line
526 begin = np.array((coords[0], coords[1]))
527 end = np.array((coords[2], coords[3]))
528 line(coords, width)
530 vec = end - begin
531 length = np.sqrt((vec[:2]**2).sum())
532 length = min(length, 0.3 * self.scale)
534 angle = np.arctan2(end[1] - begin[1], end[0] - begin[0]) + np.pi
535 x1 = (end[0] + length * np.cos(angle - 0.3)).round().astype(int)
536 y1 = (end[1] + length * np.sin(angle - 0.3)).round().astype(int)
537 x2 = (end[0] + length * np.cos(angle + 0.3)).round().astype(int)
538 y2 = (end[1] + length * np.sin(angle + 0.3)).round().astype(int)
539 line((x1, y1, end[0], end[1]), width)
540 line((x2, y2, end[0], end[1]), width)
542 def draw_axes(self):
543 axes_length = 15
545 rgb = ['red', 'green', 'blue']
547 for i in self.axes[:, 2].argsort():
548 a = 20
549 b = self.window.size[1] - 20
550 c = int(self.axes[i][0] * axes_length + a)
551 d = int(-self.axes[i][1] * axes_length + b)
552 self.window.line((a, b, c, d))
553 self.window.text(c, d, 'XYZ'[i], color=rgb[i])
555 def draw_frame_number(self):
556 x, y = self.window.size
557 self.window.text(x, y, '{0}/{1}'.format(self.frame + 1,
558 len(self.images)),
559 anchor='SE')
561 def release(self, event):
562 if event.button in [4, 5]:
563 self.scroll_event(event)
564 return
566 if event.button != self.b1:
567 return
569 selected = self.images.selected
570 selected_ordered = self.images.selected_ordered
572 if event.time < self.t0 + 200: # 200 ms
573 d = self.P - self.xy
574 r = self.get_covalent_radii()
575 hit = np.less((d**2).sum(1), (self.scale * r)**2)
576 for a in self.indices[::-1]:
577 if a < len(self.atoms) and hit[a]:
578 if event.modifier == 'ctrl':
579 selected[a] = not selected[a]
580 if selected[a]:
581 selected_ordered += [a]
582 elif len(selected_ordered) > 0:
583 if selected_ordered[-1] == a:
584 selected_ordered = selected_ordered[:-1]
585 else:
586 selected_ordered = []
587 else:
588 selected[:] = False
589 selected[a] = True
590 selected_ordered = [a]
591 break
592 else:
593 selected[:] = False
594 selected_ordered = []
595 self.draw()
596 else:
597 A = (event.x, event.y)
598 C1 = np.minimum(A, self.xy)
599 C2 = np.maximum(A, self.xy)
600 hit = np.logical_and(self.P > C1, self.P < C2)
601 indices = np.compress(hit.prod(1), np.arange(len(hit)))
602 if event.modifier != 'ctrl':
603 selected[:] = False
604 selected[indices] = True
605 if (len(indices) == 1 and
606 indices[0] not in self.images.selected_ordered):
607 selected_ordered += [indices[0]]
608 elif len(indices) > 1:
609 selected_ordered = []
610 self.draw()
612 # XXX check bounds
613 natoms = len(self.atoms)
614 indices = np.arange(natoms)[self.images.selected[:natoms]]
615 if len(indices) != len(selected_ordered):
616 selected_ordered = []
617 self.images.selected_ordered = selected_ordered
619 def press(self, event):
620 self.button = event.button
621 self.xy = (event.x, event.y)
622 self.t0 = event.time
623 self.axes0 = self.axes
624 self.center0 = self.center
626 def move(self, event):
627 x = event.x
628 y = event.y
629 x0, y0 = self.xy
630 if self.button == self.b1:
631 x0 = int(round(x0))
632 y0 = int(round(y0))
633 self.draw()
634 self.window.canvas.create_rectangle((x, y, x0, y0))
635 return
637 if event.modifier == 'shift':
638 self.center = (self.center0 -
639 np.dot(self.axes, (x - x0, y0 - y, 0)) / self.scale)
640 else:
641 # Snap mode: the a-b angle and t should multipla of 15 degrees ???
642 a = x - x0
643 b = y0 - y
644 t = sqrt(a * a + b * b)
645 if t > 0:
646 a /= t
647 b /= t
648 else:
649 a = 1.0
650 b = 0.0
651 c = cos(0.01 * t)
652 s = -sin(0.01 * t)
653 rotation = np.array([(c * a * a + b * b, (c - 1) * b * a, s * a),
654 ((c - 1) * a * b, c * b * b + a * a, s * b),
655 (-s * a, -s * b, c)])
656 self.axes = np.dot(self.axes0, rotation)
657 if len(self.atoms) > 0:
658 com = self.X_pos.mean(0)
659 else:
660 com = self.atoms.cell.mean(0)
661 self.center = com - np.dot(com - self.center0,
662 np.dot(self.axes0, self.axes.T))
663 self.draw(status=False)
665 def render_window(self):
666 return Render(self)
668 def resize(self, event):
669 w, h = self.window.size
670 self.scale *= (event.width * event.height / (w * h))**0.5
671 self.window.size[:] = [event.width, event.height]
672 self.draw()