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 # Structures: Abbreviation, name, 

87 # 4-index (boolean), two lattice const (bool), factory 

88 structure_data = (('fcc', _('Face centered cubic (fcc)'), 

89 False, False, FaceCenteredCubic), 

90 ('bcc', _('Body centered cubic (bcc)'), 

91 False, False, BodyCenteredCubic), 

92 ('sc', _('Simple cubic (sc)'), 

93 False, False, SimpleCubic), 

94 ('hcp', _('Hexagonal closed-packed (hcp)'), 

95 True, True, HexagonalClosedPacked), 

96 ('graphite', _('Graphite'), 

97 True, True, Graphite)) 

98 # NB: HCP is broken! 

99 

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

101 import_names = { 

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

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

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

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

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

107 

108 # Default layer specifications for the different structures. 

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

110 ((1, 1, 0), 9), 

111 ((1, 1, 1), 5)], 

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

113 ((1, 1, 0), 9), 

114 ((1, 1, 1), 5)], 

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

116 ((1, 1, 0), 9), 

117 ((1, 1, 1), 5)], 

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

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

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

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

122 

123 def __init__(self, gui): 

124 self.atoms = None 

125 self.no_update = True 

126 self.old_structure = 'undefined' 

127 

128 win = self.win = ui.Window(_('Nanoparticle')) 

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

130 

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

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

133 self.set_structure_data) 

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

135 win.add(self.element) 

136 win.add(self.elementinfo) 

137 win.add(lattice_button) 

138 

139 # The structure and lattice constant 

140 labels = [] 

141 values = [] 

142 self.needs_4index = {} 

143 self.needs_2lat = {} 

144 self.factory = {} 

145 for abbrev, name, n4, c, factory in self.structure_data: 

146 labels.append(name) 

147 values.append(abbrev) 

148 self.needs_4index[abbrev] = n4 

149 self.needs_2lat[abbrev] = c 

150 self.factory[abbrev] = factory 

151 self.structure = ui.ComboBox(labels, values, self.update_structure) 

152 win.add([_('Structure:'), self.structure]) 

153 self.fourindex = self.needs_4index[values[0]] 

154 

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

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

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

158 

159 # Choose specification method 

160 self.method = ui.ComboBox( 

161 [_('Layer specification'), _('Wulff construction')], 

162 ['layers', 'wulff'], 

163 self.update_gui_method) 

164 win.add([_('Method: '), self.method]) 

165 

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

167 win.add(self.layerlabel) 

168 self.direction_table_rows = ui.Rows() 

169 win.add(self.direction_table_rows) 

170 self.default_direction_table() 

171 

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

173 self.new_direction_and_size_rows = ui.Rows() 

174 win.add(self.new_direction_and_size_rows) 

175 self.update_new_direction_and_size_stuff() 

176 

177 # Information 

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

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

180 ui.Label('-'), 

181 _(' Approx. diameter: '), 

182 ui.Label('-')] 

183 win.add(self.info) 

184 

185 # Finalize setup 

186 self.update_structure('fcc') 

187 self.update_gui_method() 

188 self.no_update = False 

189 

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

191 win.add(self.auto) 

192 

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

194 ui.helpbutton(helptext), 

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

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

197 

198 self.gui = gui 

199 self.smaller_button = None 

200 self.largeer_button = None 

201 

202 self.element.grab_focus() 

203 

204 def default_direction_table(self): 

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

206 self.direction_table = [] 

207 struct = self.structure.value 

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

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

210 

211 def update_direction_table(self): 

212 self.direction_table_rows.clear() 

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

214 self.add_direction(direction, layers, energy) 

215 self.update() 

216 

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

218 i = len(self.direction_table_rows) 

219 

220 if self.method.value == 'wulff': 

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

222 else: 

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

224 

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

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

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

228 

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

230 spin, up, down, delete]) 

231 up.active = i > 0 

232 down.active = False 

233 delete.active = i > 0 

234 

235 if i > 0: 

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

237 down.active = True 

238 delete.active = True 

239 

240 def update_new_direction_and_size_stuff(self): 

241 if self.needs_4index[self.structure.value]: 

242 n = 4 

243 else: 

244 n = 3 

245 

246 rows = self.new_direction_and_size_rows 

247 

248 rows.clear() 

249 

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

251 for i in range(n): 

252 if i > 0: 

253 row.append(',') 

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

255 row.append('):') 

256 

257 if self.method.value == 'wulff': 

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

259 else: 

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

261 

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

263 

264 rows.add(row) 

265 

266 if self.method.value == 'wulff': 

267 # Extra widgets for the Wulff construction 

268 self.size_radio = ui.RadioButtons( 

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

270 ['natoms', 'diameter'], 

271 self.update_gui_size) 

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

273 self.update_size_natoms) 

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

275 self.update_size_diameter) 

276 self.round_radio = ui.RadioButtons( 

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

278 ['above', 'below', 'closest'], 

279 callback=self.update) 

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

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

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

283 rows.add(self.size_radio) 

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

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

286 rows.add( 

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

288 rows.add(self.round_radio) 

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

290 self.update_gui_size() 

291 else: 

292 self.smaller_button = None 

293 self.larger_button = None 

294 

295 def update_structure(self, s): 

296 'Called when the user changes the structure.' 

297 # s = self.structure.value 

298 if s != self.old_structure: 

299 old4 = self.fourindex 

300 self.fourindex = self.needs_4index[s] 

301 if self.fourindex != old4: 

302 # The table of directions is invalid. 

303 self.default_direction_table() 

304 self.old_structure = s 

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

306 

307 self.update() 

308 

309 def update_gui_method(self, *args): 

310 'Switch between layer specification and Wulff construction.' 

311 self.update_direction_table() 

312 self.update_new_direction_and_size_stuff() 

313 if self.method.value == 'wulff': 

314 self.layerlabel.text = _( 

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

316 else: 

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

318 

319 self.update() 

320 

321 def wulff_smaller(self, widget=None): 

322 'Make a smaller Wulff construction.' 

323 n = len(self.atoms) 

324 self.size_radio.value = 'natoms' 

325 self.size_natoms.value = n - 1 

326 self.round_radio.value = 'below' 

327 self.apply() 

328 

329 def wulff_larger(self, widget=None): 

330 'Make a larger Wulff construction.' 

331 n = len(self.atoms) 

332 self.size_radio.value = 'natoms' 

333 self.size_natoms.value = n + 1 

334 self.round_radio.value = 'above' 

335 self.apply() 

336 

337 def row_add(self, widget=None): 

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

339 if self.fourindex: 

340 n = 4 

341 else: 

342 n = 3 

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

344 if not any(idx): 

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

346 return 

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

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

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

350 return 

351 new = [idx, 5, 1.0] 

352 if self.method.value == 'wulff': 

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

354 else: 

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

356 self.direction_table.append(new) 

357 self.add_direction(*new) 

358 self.update() 

359 

360 def row_delete(self, row): 

361 del self.direction_table[row] 

362 self.update_direction_table() 

363 

364 def row_swap_next(self, row): 

365 dt = self.direction_table 

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

367 self.update_direction_table() 

368 

369 def update_gui_size(self, widget=None): 

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

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

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

373 

374 def update_size_natoms(self, widget=None): 

375 at_vol = self.get_atomic_volume() 

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

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

378 self.size_diameter.value = dia 

379 self.update() 

380 

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

382 if self.size_diameter.active: 

383 at_vol = self.get_atomic_volume() 

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

385 self.size_natoms.value = int(n) 

386 if update: 

387 self.update() 

388 

389 def update(self, *args): 

390 if self.no_update: 

391 return 

392 self.element.Z # Check 

393 if self.auto.value: 

394 self.makeatoms() 

395 if self.atoms is not None: 

396 self.gui.new_atoms(self.atoms) 

397 else: 

398 self.clearatoms() 

399 self.makeinfo() 

400 

401 def set_structure_data(self, *args): 

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

403 z = self.element.Z 

404 if z is None: 

405 return 

406 ref = ase.data.reference_states[z] 

407 if ref is None: 

408 structure = None 

409 else: 

410 structure = ref['symmetry'] 

411 

412 if ref is None or structure not in [s[0] 

413 for s in self.structure_data]: 

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

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

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

417 return 

418 

419 self.structure.value = structure 

420 

421 a = ref['a'] 

422 self.a.value = a 

423 self.fourindex = self.needs_4index[structure] 

424 if self.fourindex: 

425 try: 

426 c = ref['c'] 

427 except KeyError: 

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

429 self.c.value = c 

430 

431 def makeatoms(self, *args): 

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

433 symbol = self.element.symbol 

434 if symbol is None: 

435 self.clearatoms() 

436 self.makeinfo() 

437 return False 

438 struct = self.structure.value 

439 if self.needs_2lat[struct]: 

440 # a and c lattice constants 

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

442 'c': self.c.value} 

443 lc_str = str(lc) 

444 else: 

445 lc = self.a.value 

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

447 if self.method.value == 'wulff': 

448 # Wulff construction 

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

450 surfaceenergies = [x[1].value 

451 for x in self.direction_table_rows.rows] 

452 self.update_size_diameter(update=False) 

453 rounding = self.round_radio.value 

454 self.atoms = wulff_construction(symbol, 

455 surfaces, 

456 surfaceenergies, 

457 self.size_natoms.value, 

458 self.factory[struct], 

459 rounding, lc) 

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

461 'surfaces': str(surfaces), 

462 'energies': str(surfaceenergies), 

463 'latconst': lc_str, 

464 'natoms': self.size_natoms.value, 

465 'structure': struct, 

466 'rounding': rounding} 

467 else: 

468 # Layer-by-layer specification 

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

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

471 self.atoms = self.factory[struct](symbol, 

472 copy(surfaces), 

473 layers, latticeconstant=lc) 

474 imp = self.import_names[struct] 

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

476 'element': symbol, 

477 'surfaces': str(surfaces), 

478 'layers': str(layers), 

479 'latconst': lc_str, 

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

481 self.makeinfo() 

482 

483 return python 

484 

485 def clearatoms(self): 

486 self.atoms = None 

487 

488 def get_atomic_volume(self): 

489 s = self.structure.value 

490 a = self.a.value 

491 c = self.c.value 

492 if s == 'fcc': 

493 return a**3 / 4 

494 elif s == 'bcc': 

495 return a**3 / 2 

496 elif s == 'sc': 

497 return a**3 

498 elif s == 'hcp': 

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

500 elif s == 'graphite': 

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

502 

503 def makeinfo(self): 

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

505 

506 Also turns the Wulff construction buttons [Larger] and 

507 [Smaller] on and off. 

508 """ 

509 if self.atoms is None: 

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

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

512 else: 

513 at_vol = self.get_atomic_volume() 

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

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

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

517 

518 if self.method.value == 'wulff': 

519 if self.smaller_button is not None: 

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

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

522 

523 def apply(self, callbackarg=None): 

524 self.makeatoms() 

525 if self.atoms is not None: 

526 self.gui.new_atoms(self.atoms) 

527 return True 

528 else: 

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

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

531 'parameters.')) 

532 return False 

533 

534 def ok(self): 

535 if self.apply(): 

536 self.win.close()