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

1"""nanoparticle.py - Window for setting up crystalline nanoparticles. 

2""" 

3from copy import copy 

4from ase.gui.i18n import _ 

5 

6import numpy as np 

7 

8import ase 

9import ase.data 

10import ase.gui.ui as ui 

11 

12# Delayed imports: 

13# ase.cluster.data 

14 

15from ase.cluster.cubic import FaceCenteredCubic, BodyCenteredCubic, SimpleCubic 

16from ase.cluster.hexagonal import HexagonalClosedPacked, Graphite 

17from ase.cluster import wulff_construction 

18from ase.gui.widgets import Element, pybutton 

19 

20 

21introtext = _("""\ 

22Create a nanoparticle either by specifying the number of layers, or using the 

23Wulff construction. Please press the [Help] button for instructions on how to 

24specify the directions. 

25WARNING: The Wulff construction currently only works with cubic crystals! 

26""") 

27 

28helptext = _(""" 

29The nanoparticle module sets up a nano-particle or a cluster with a given 

30crystal structure. 

31 

321) Select the element, the crystal structure and the lattice constant(s). 

33 The [Get structure] button will find the data for a given element. 

34 

352) Choose if you want to specify the number of layers in each direction, or if 

36 you want to use the Wulff construction. In the latter case, you must 

37 specify surface energies in each direction, and the size of the cluster. 

38 

39How to specify the directions: 

40------------------------------ 

41 

42First time a direction appears, it is interpreted as the entire family of 

43directions, i.e. (0,0,1) also covers (1,0,0), (-1,0,0) etc. If one of these 

44directions is specified again, the second specification overrules that specific 

45direction. For this reason, the order matters and you can rearrange the 

46directions with the [Up] and [Down] keys. You can also add a new direction, 

47remember to press [Add] or it will not be included. 

48 

49Example: (1,0,0) (1,1,1), (0,0,1) would specify the {100} family of directions, 

50the {111} family and then the (001) direction, overruling the value given for 

51the whole family of directions. 

52""") 

53 

54py_template_layers = """ 

55import ase 

56%(import)s 

57 

58surfaces = %(surfaces)s 

59layers = %(layers)s 

60lc = %(latconst)s 

61atoms = %(factory)s('%(element)s', surfaces, layers, latticeconstant=lc) 

62 

63# OPTIONAL: Cast to ase.Atoms object, discarding extra information: 

64# atoms = ase.Atoms(atoms) 

65""" 

66 

67py_template_wulff = """ 

68import ase 

69from ase.cluster import wulff_construction 

70 

71surfaces = %(surfaces)s 

72esurf = %(energies)s 

73lc = %(latconst)s 

74size = %(natoms)s # Number of atoms 

75atoms = wulff_construction('%(element)s', surfaces, esurf, 

76 size, '%(structure)s', 

77 rounding='%(rounding)s', latticeconstant=lc) 

78 

79# OPTIONAL: Cast to ase.Atoms object, discarding extra information: 

80# atoms = ase.Atoms(atoms) 

81""" 

82 

83 

84class SetupNanoparticle: 

85 "Window for setting up a nanoparticle." 

86 

87 structure_names = { 

88 'fcc': _('Face centered cubic (fcc)'), 

89 'bcc': _('Body centered cubic (bcc)'), 

90 'sc': _('Simple cubic (sc)'), 

91 'hcp': _('Hexagonal closed-packed (hcp)'), 

92 'graphite': _('Graphite')} 

93 

94 needs_4index = { # 3 or 4 index dimension 

95 'fcc': False, 'bcc': False, 'sc': False, 

96 'hcp': True, 'graphite': True} 

97 

98 needs_2lat = { # 1 or 2 lattice parameters 

99 'fcc': False, 'bcc': False, 'sc': False, 

100 'hcp': True, 'graphite': True} 

101 

102 structure_factories = { 

103 'fcc': FaceCenteredCubic, 

104 'bcc': BodyCenteredCubic, 

105 'sc': SimpleCubic, 

106 'hcp': HexagonalClosedPacked, 

107 'graphite': Graphite} 

108 

109 # A list of import statements for the Python window. 

110 import_names = { 

111 'fcc': 'from ase.cluster.cubic import FaceCenteredCubic', 

112 'bcc': 'from ase.cluster.cubic import BodyCenteredCubic', 

113 'sc': 'from ase.cluster.cubic import SimpleCubic', 

114 'hcp': 'from ase.cluster.hexagonal import HexagonalClosedPacked', 

115 'graphite': 'from ase.cluster.hexagonal import Graphite'} 

116 

117 # Default layer specifications for the different structures. 

118 default_layers = {'fcc': [((1, 0, 0), 6), 

119 ((1, 1, 0), 9), 

120 ((1, 1, 1), 5)], 

121 'bcc': [((1, 0, 0), 6), 

122 ((1, 1, 0), 9), 

123 ((1, 1, 1), 5)], 

124 'sc': [((1, 0, 0), 6), 

125 ((1, 1, 0), 9), 

126 ((1, 1, 1), 5)], 

127 'hcp': [((0, 0, 0, 1), 5), 

128 ((1, 0, -1, 0), 5)], 

129 'graphite': [((0, 0, 0, 1), 5), 

130 ((1, 0, -1, 0), 5)]} 

131 

132 def __init__(self, gui): 

133 self.atoms = None 

134 self.no_update = True 

135 self.old_structure = 'fcc' 

136 

137 win = self.win = ui.Window(_('Nanoparticle'), wmtype='utility') 

138 win.add(ui.Text(introtext)) 

139 

140 self.element = Element('', self.apply) 

141 lattice_button = ui.Button(_('Get structure'), 

142 self.set_structure_data) 

143 self.elementinfo = ui.Label(' ') 

144 win.add(self.element) 

145 win.add(self.elementinfo) 

146 win.add(lattice_button) 

147 

148 # The structure and lattice constant 

149 labels = [] 

150 values = [] 

151 for abbrev, name in self.structure_names.items(): 

152 labels.append(name) 

153 values.append(abbrev) 

154 self.structure_cb = ui.ComboBox( 

155 labels=labels, values=values, callback=self.update_structure) 

156 win.add([_('Structure:'), self.structure_cb]) 

157 

158 self.a = ui.SpinBox(3.0, 0.0, 1000.0, 0.01, self.update) 

159 self.c = ui.SpinBox(3.0, 0.0, 1000.0, 0.01, self.update) 

160 win.add([_('Lattice constant: a ='), self.a, ' c =', self.c]) 

161 

162 # Choose specification method 

163 self.method_cb = ui.ComboBox( 

164 labels=[_('Layer specification'), _('Wulff construction')], 

165 values=['layers', 'wulff'], 

166 callback=self.update_gui_method) 

167 win.add([_('Method: '), self.method_cb]) 

168 

169 self.layerlabel = ui.Label('Missing text') # Filled in later 

170 win.add(self.layerlabel) 

171 self.direction_table_rows = ui.Rows() 

172 win.add(self.direction_table_rows) 

173 self.default_direction_table() 

174 

175 win.add(_('Add new direction:')) 

176 self.new_direction_and_size_rows = ui.Rows() 

177 win.add(self.new_direction_and_size_rows) 

178 self.update_new_direction_and_size_stuff() 

179 

180 # Information 

181 win.add(_('Information about the created cluster:')) 

182 self.info = [_('Number of atoms: '), 

183 ui.Label('-'), 

184 _(' Approx. diameter: '), 

185 ui.Label('-')] 

186 win.add(self.info) 

187 

188 # Finalize setup 

189 self.update_structure() 

190 self.update_gui_method() 

191 self.no_update = False 

192 

193 self.auto = ui.CheckButton(_('Automatic Apply')) 

194 win.add(self.auto) 

195 

196 win.add([pybutton(_('Creating a nanoparticle.'), self.makeatoms), 

197 ui.helpbutton(helptext), 

198 ui.Button(_('Apply'), self.apply), 

199 ui.Button(_('OK'), self.ok)]) 

200 

201 self.gui = gui 

202 self.smaller_button = None 

203 self.largeer_button = None 

204 

205 self.element.grab_focus() 

206 

207 def default_direction_table(self): 

208 'Set default directions and values for the current crystal structure.' 

209 self.direction_table = [] 

210 struct = self.structure_cb.value 

211 for direction, layers in self.default_layers[struct]: 

212 self.direction_table.append((direction, layers, 1.0)) 

213 

214 def update_direction_table(self): 

215 self.direction_table_rows.clear() 

216 for direction, layers, energy in self.direction_table: 

217 self.add_direction(direction, layers, energy) 

218 self.update() 

219 

220 def add_direction(self, direction, layers, energy): 

221 i = len(self.direction_table_rows) 

222 

223 if self.method_cb.value == 'wulff': 

224 spin = ui.SpinBox(energy, 0.0, 1000.0, 0.1, self.update) 

225 else: 

226 spin = ui.SpinBox(layers, 1, 100, 1, self.update) 

227 

228 up = ui.Button(_('Up'), self.row_swap_next, i - 1) 

229 down = ui.Button(_('Down'), self.row_swap_next, i) 

230 delete = ui.Button(_('Delete'), self.row_delete, i) 

231 

232 self.direction_table_rows.add([str(direction) + ':', 

233 spin, up, down, delete]) 

234 up.active = i > 0 

235 down.active = False 

236 delete.active = i > 0 

237 

238 if i > 0: 

239 down, delete = self.direction_table_rows[-2][3:] 

240 down.active = True 

241 delete.active = True 

242 

243 def update_new_direction_and_size_stuff(self): 

244 if self.needs_4index[self.structure_cb.value]: 

245 n = 4 

246 else: 

247 n = 3 

248 

249 rows = self.new_direction_and_size_rows 

250 

251 rows.clear() 

252 

253 self.new_direction = row = ['('] 

254 for i in range(n): 

255 if i > 0: 

256 row.append(',') 

257 row.append(ui.SpinBox(0, -100, 100, 1)) 

258 row.append('):') 

259 

260 if self.method_cb.value == 'wulff': 

261 row.append(ui.SpinBox(1.0, 0.0, 1000.0, 0.1)) 

262 else: 

263 row.append(ui.SpinBox(5, 1, 100, 1)) 

264 

265 row.append(ui.Button(_('Add'), self.row_add)) 

266 

267 rows.add(row) 

268 

269 if self.method_cb.value == 'wulff': 

270 # Extra widgets for the Wulff construction 

271 self.size_radio = ui.RadioButtons( 

272 [_('Number of atoms'), _('Diameter')], 

273 ['natoms', 'diameter'], 

274 self.update_gui_size) 

275 self.size_natoms = ui.SpinBox(100, 1, 100000, 1, 

276 self.update_size_natoms) 

277 self.size_diameter = ui.SpinBox(5.0, 0, 100.0, 0.1, 

278 self.update_size_diameter) 

279 self.round_radio = ui.RadioButtons( 

280 [_('above '), _('below '), _('closest ')], 

281 ['above', 'below', 'closest'], 

282 callback=self.update) 

283 self.smaller_button = ui.Button(_('Smaller'), self.wulff_smaller) 

284 self.larger_button = ui.Button(_('Larger'), self.wulff_larger) 

285 rows.add(_('Choose size using:')) 

286 rows.add(self.size_radio) 

287 rows.add([_('atoms'), self.size_natoms, 

288 _(u'ų'), self.size_diameter]) 

289 rows.add( 

290 _('Rounding: If exact size is not possible, choose the size:')) 

291 rows.add(self.round_radio) 

292 rows.add([self.smaller_button, self.larger_button]) 

293 self.update_gui_size() 

294 else: 

295 self.smaller_button = None 

296 self.larger_button = None 

297 

298 def update_structure(self, s=None): 

299 'Called when the user changes the structure.' 

300 s = self.structure_cb.value 

301 if s != self.old_structure: 

302 old4 = self.needs_4index[self.old_structure] 

303 if self.needs_4index[s] != old4: 

304 # The table of directions is invalid. 

305 self.update_new_direction_and_size_stuff() 

306 self.default_direction_table() 

307 self.update_direction_table() 

308 self.old_structure = s 

309 self.c.active = self.needs_2lat[s] 

310 self.update() 

311 

312 def update_gui_method(self, *args): 

313 'Switch between layer specification and Wulff construction.' 

314 self.update_direction_table() 

315 self.update_new_direction_and_size_stuff() 

316 if self.method_cb.value == 'wulff': 

317 self.layerlabel.text = _( 

318 'Surface energies (as energy/area, NOT per atom):') 

319 else: 

320 self.layerlabel.text = _('Number of layers:') 

321 

322 self.update() 

323 

324 def wulff_smaller(self, widget=None): 

325 'Make a smaller Wulff construction.' 

326 n = len(self.atoms) 

327 self.size_radio.value = 'natoms' 

328 self.size_natoms.value = n - 1 

329 self.round_radio.value = 'below' 

330 self.apply() 

331 

332 def wulff_larger(self, widget=None): 

333 'Make a larger Wulff construction.' 

334 n = len(self.atoms) 

335 self.size_radio.value = 'natoms' 

336 self.size_natoms.value = n + 1 

337 self.round_radio.value = 'above' 

338 self.apply() 

339 

340 def row_add(self, widget=None): 

341 'Add a row to the list of directions.' 

342 if self.needs_4index[self.structure_cb.value]: 

343 n = 4 

344 else: 

345 n = 3 

346 idx = tuple(a.value for a in self.new_direction[1:1 + 2 * n:2]) 

347 if not any(idx): 

348 ui.error(_('At least one index must be non-zero'), '') 

349 return 

350 if n == 4 and sum(idx) != 0: 

351 ui.error(_('Invalid hexagonal indices', 

352 'The sum of the first three numbers must be zero')) 

353 return 

354 new = [idx, 5, 1.0] 

355 if self.method_cb.value == 'wulff': 

356 new[1] = self.new_direction[-2].value 

357 else: 

358 new[2] = self.new_direction[-2].value 

359 self.direction_table.append(new) 

360 self.add_direction(*new) 

361 self.update() 

362 

363 def row_delete(self, row): 

364 del self.direction_table[row] 

365 self.update_direction_table() 

366 

367 def row_swap_next(self, row): 

368 dt = self.direction_table 

369 dt[row], dt[row + 1] = dt[row + 1], dt[row] 

370 self.update_direction_table() 

371 

372 def update_gui_size(self, widget=None): 

373 'Update gui when the cluster size specification changes.' 

374 self.size_natoms.active = self.size_radio.value == 'natoms' 

375 self.size_diameter.active = self.size_radio.value == 'diameter' 

376 

377 def update_size_natoms(self, widget=None): 

378 at_vol = self.get_atomic_volume() 

379 dia = 2.0 * (3 * self.size_natoms.value * at_vol / 

380 (4 * np.pi))**(1 / 3) 

381 self.size_diameter.value = dia 

382 self.update() 

383 

384 def update_size_diameter(self, widget=None, update=True): 

385 if self.size_diameter.active: 

386 at_vol = self.get_atomic_volume() 

387 n = round(np.pi / 6 * self.size_diameter.value**3 / at_vol) 

388 self.size_natoms.value = int(n) 

389 if update: 

390 self.update() 

391 

392 def update(self, *args): 

393 if self.no_update: 

394 return 

395 self.element.Z # Check 

396 if self.auto.value: 

397 self.makeatoms() 

398 if self.atoms is not None: 

399 self.gui.new_atoms(self.atoms) 

400 else: 

401 self.clearatoms() 

402 self.makeinfo() 

403 

404 def set_structure_data(self, *args): 

405 'Called when the user presses [Get structure].' 

406 z = self.element.Z 

407 if z is None: 

408 return 

409 ref = ase.data.reference_states[z] 

410 if ref is None: 

411 structure = None 

412 else: 

413 structure = ref['symmetry'] 

414 

415 if ref is None or structure not in self.structure_names: 

416 ui.error(_('Unsupported or unknown structure'), 

417 _('Element = {0}, structure = {1}') 

418 .format(self.element.symbol, structure)) 

419 return 

420 

421 self.structure_cb.value = self.structure_names[structure] 

422 

423 a = ref['a'] 

424 self.a.value = a 

425 if self.needs_4index[structure]: 

426 try: 

427 c = ref['c'] 

428 except KeyError: 

429 c = ref['c/a'] * a 

430 self.c.value = c 

431 

432 self.update_structure() 

433 

434 def makeatoms(self, *args): 

435 'Make the atoms according to the current specification.' 

436 symbol = self.element.symbol 

437 if symbol is None: 

438 self.clearatoms() 

439 self.makeinfo() 

440 return False 

441 struct = self.structure_cb.value 

442 if self.needs_2lat[struct]: 

443 # a and c lattice constants 

444 lc = {'a': self.a.value, 

445 'c': self.c.value} 

446 lc_str = str(lc) 

447 else: 

448 lc = self.a.value 

449 lc_str = '%.5f' % (lc,) 

450 if self.method_cb.value == 'wulff': 

451 # Wulff construction 

452 surfaces = [x[0] for x in self.direction_table] 

453 surfaceenergies = [x[1].value 

454 for x in self.direction_table_rows.rows] 

455 self.update_size_diameter(update=False) 

456 rounding = self.round_radio.value 

457 self.atoms = wulff_construction(symbol, 

458 surfaces, 

459 surfaceenergies, 

460 self.size_natoms.value, 

461 self.structure_factories[struct], 

462 rounding, lc) 

463 python = py_template_wulff % {'element': symbol, 

464 'surfaces': str(surfaces), 

465 'energies': str(surfaceenergies), 

466 'latconst': lc_str, 

467 'natoms': self.size_natoms.value, 

468 'structure': struct, 

469 'rounding': rounding} 

470 else: 

471 # Layer-by-layer specification 

472 surfaces = [x[0] for x in self.direction_table] 

473 layers = [x[1].value for x in self.direction_table_rows.rows] 

474 self.atoms = self.structure_factories[struct]( 

475 symbol, copy(surfaces), layers, latticeconstant=lc) 

476 imp = self.import_names[struct] 

477 python = py_template_layers % {'import': imp, 

478 'element': symbol, 

479 'surfaces': str(surfaces), 

480 'layers': str(layers), 

481 'latconst': lc_str, 

482 'factory': imp.split()[-1]} 

483 self.makeinfo() 

484 

485 return python 

486 

487 def clearatoms(self): 

488 self.atoms = None 

489 

490 def get_atomic_volume(self): 

491 s = self.structure_cb.value 

492 a = self.a.value 

493 c = self.c.value 

494 if s == 'fcc': 

495 return a**3 / 4 

496 elif s == 'bcc': 

497 return a**3 / 2 

498 elif s == 'sc': 

499 return a**3 

500 elif s == 'hcp': 

501 return np.sqrt(3.0) / 2 * a * a * c / 2 

502 elif s == 'graphite': 

503 return np.sqrt(3.0) / 2 * a * a * c / 4 

504 

505 def makeinfo(self): 

506 """Fill in information field about the atoms. 

507 

508 Also turns the Wulff construction buttons [Larger] and 

509 [Smaller] on and off. 

510 """ 

511 if self.atoms is None: 

512 self.info[1].text = '-' 

513 self.info[3].text = '-' 

514 else: 

515 at_vol = self.get_atomic_volume() 

516 dia = 2 * (3 * len(self.atoms) * at_vol / (4 * np.pi))**(1 / 3) 

517 self.info[1].text = str(len(self.atoms)) 

518 self.info[3].text = u'{0:.1f} Å'.format(dia) 

519 

520 if self.method_cb.value == 'wulff': 

521 if self.smaller_button is not None: 

522 self.smaller_button.active = self.atoms is not None 

523 self.larger_button.active = self.atoms is not None 

524 

525 def apply(self, callbackarg=None): 

526 self.makeatoms() 

527 if self.atoms is not None: 

528 self.gui.new_atoms(self.atoms) 

529 return True 

530 else: 

531 ui.error(_('No valid atoms.'), 

532 _('You have not (yet) specified a consistent set of ' 

533 'parameters.')) 

534 return False 

535 

536 def ok(self): 

537 if self.apply(): 

538 self.win.close()