Hide keyboard shortcuts

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 

3 

4import numpy as np 

5 

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 

16 

17GREEN = '#74DF00' 

18PURPLE = '#AC58FA' 

19BLACKISH = '#151515' 

20 

21 

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) 

40 

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 

54 

55 

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 

62 

63 bonds = np.empty((nbonds, 5), int) 

64 if nbonds == 0: 

65 return bonds 

66 

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 

75 

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 

82 

83 

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 

91 

92 # XXX 

93 self.colormode = 'jmol' 

94 self.colors = {} 

95 

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))) 

99 

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'] 

103 

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 

110 

111 @property 

112 def atoms(self): 

113 return self.images[self.frame] 

114 

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]) 

121 

122 fname = self.images.filenames[frame] 

123 if fname is None: 

124 title = 'ase.gui' 

125 else: 

126 title = basename(fname) 

127 

128 self.window.title = title 

129 

130 self.call_observers() 

131 

132 if focus: 

133 self.focus() 

134 else: 

135 self.draw() 

136 

137 def set_atoms(self, atoms): 

138 natoms = len(atoms) 

139 

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)) 

145 

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) 

152 

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. 

157 

158 # Also B are the end points of line segments. 

159 

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):] 

165 

166 if 1: # if init or frame != self.frame: 

167 cell = atoms.cell 

168 ncellparts = len(B1) 

169 nbonds = len(bonds) 

170 

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) 

175 

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 

189 

190 def showing_bonds(self): 

191 return self.window['toggle-show-bonds'] 

192 

193 def showing_cell(self): 

194 return self.window['toggle-show-unit-cell'] 

195 

196 def toggle_show_unit_cell(self, key=None): 

197 self.set_frame() 

198 

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() 

212 

213 def show_labels(self): 

214 self.update_labels() 

215 self.draw() 

216 

217 def toggle_show_axes(self, key=None): 

218 self.draw() 

219 

220 def toggle_show_bonds(self, key=None): 

221 self.set_frame() 

222 

223 def toggle_show_velocities(self, key=None): 

224 self.draw() 

225 

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)) 

233 

234 def toggle_show_forces(self, key=None): 

235 self.draw() 

236 

237 def hide_selected(self): 

238 self.images.visible[self.images.selected] = False 

239 self.draw() 

240 

241 def show_selected(self): 

242 self.images.visible[self.images.selected] = True 

243 self.draw() 

244 

245 def repeat_window(self, key=None): 

246 return Repeat(self) 

247 

248 def rotate_window(self): 

249 return Rotate(self) 

250 

251 def colors_window(self, key=None): 

252 win = ColorWindow(self) 

253 self.register_vulnerable(win) 

254 return win 

255 

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 

264 

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() 

286 

287 def reset_view(self, menuitem): 

288 self.axes = rotate('0.0x,0.0y,0.0z') 

289 self.set_frame() 

290 self.focus(self) 

291 

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 

318 

319 A = complete_cell(self.atoms.cell) 

320 x1 = A[i] 

321 x2 = A[j] 

322 

323 norm = np.linalg.norm 

324 

325 x1 = x1 / norm(x1) 

326 x2 = x2 - x1 * np.dot(x1, x2) 

327 x2 /= norm(x2) 

328 x3 = np.cross(x1, x2) 

329 

330 self.axes = np.array([x1, x2, x3]).T 

331 

332 self.set_frame() 

333 

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()] 

338 

339 if self.colormode == 'jmol': 

340 return [self.colors.get(Z, BLACKISH) for Z in self.atoms.numbers] 

341 

342 if self.colormode == 'neighbors': 

343 return [self.colors.get(Z, BLACKISH) 

344 for Z in self.get_color_scalars()] 

345 

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)] 

357 

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)) 

381 

382 def get_covalent_radii(self, atoms=None): 

383 if atoms is None: 

384 atoms = self.atoms 

385 return self.images.get_radii(atoms) 

386 

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) 

394 

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) 

407 

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) 

417 

418 for array in vector_arrays: 

419 array[:] = np.dot(array, axes) + X[:n] 

420 

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) 

426 

427 selected = self.images.selected 

428 visible = self.images.visible 

429 ncell = len(self.X_cell) 

430 bond_linewidth = self.scale * 0.15 

431 

432 self.update_labels() 

433 

434 if self.arrowkey_mode == self.ARROWKEY_MOVE: 

435 movecolor = GREEN 

436 elif self.arrowkey_mode == self.ARROWKEY_ROTATE: 

437 movecolor = PURPLE 

438 

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) 

478 

479 circle(colors[a], selected[a], 

480 A[a, 0], A[a, 1], A[a, 0] + ra, A[a, 1] + ra) 

481 

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])) 

487 

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)) 

496 

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) 

512 

513 if self.window['toggle-show-axes']: 

514 self.draw_axes() 

515 

516 if len(self.images) > 1: 

517 self.draw_frame_number() 

518 

519 self.window.update() 

520 

521 if status: 

522 self.status(self.atoms) 

523 

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) 

529 

530 vec = end - begin 

531 length = np.sqrt((vec[:2]**2).sum()) 

532 length = min(length, 0.3 * self.scale) 

533 

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) 

541 

542 def draw_axes(self): 

543 axes_length = 15 

544 

545 rgb = ['red', 'green', 'blue'] 

546 

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]) 

554 

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') 

560 

561 def release(self, event): 

562 if event.button in [4, 5]: 

563 self.scroll_event(event) 

564 return 

565 

566 if event.button != self.b1: 

567 return 

568 

569 selected = self.images.selected 

570 selected_ordered = self.images.selected_ordered 

571 

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() 

611 

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 

618 

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 

625 

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 

636 

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) 

664 

665 def render_window(self): 

666 return Render(self) 

667 

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()