Coverage for /builds/kinetik161/ase/ase/gui/nanoparticle.py: 78.69%

291 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

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

2""" 

3from copy import copy 

4 

5import numpy as np 

6 

7import ase 

8import ase.data 

9import ase.gui.ui as ui 

10from ase.cluster import wulff_construction 

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

12from ase.cluster.hexagonal import Graphite, HexagonalClosedPacked 

13from ase.gui.i18n import _ 

14from ase.gui.widgets import Element, pybutton 

15 

16# Delayed imports: 

17# ase.cluster.data 

18 

19 

20introtext = _("""\ 

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

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

23specify the directions. 

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

25""") 

26 

27helptext = _(""" 

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

29crystal structure. 

30 

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

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

33 

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

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

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

37 

38How to specify the directions: 

39------------------------------ 

40 

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

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

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

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

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

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

47 

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

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

50the whole family of directions. 

51""") 

52 

53py_template_layers = """ 

54import ase 

55%(import)s 

56 

57surfaces = %(surfaces)s 

58layers = %(layers)s 

59lc = %(latconst)s 

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

61 

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

63# atoms = ase.Atoms(atoms) 

64""" 

65 

66py_template_wulff = """ 

67import ase 

68from ase.cluster import wulff_construction 

69 

70surfaces = %(surfaces)s 

71esurf = %(energies)s 

72lc = %(latconst)s 

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

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

75 size, '%(structure)s', 

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

77 

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

79# atoms = ase.Atoms(atoms) 

80""" 

81 

82 

83class SetupNanoparticle: 

84 "Window for setting up a nanoparticle." 

85 

86 structure_names = { 

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

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

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

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

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

92 

93 needs_4index = { # 3 or 4 index dimension 

94 'fcc': False, 'bcc': False, 'sc': False, 

95 'hcp': True, 'graphite': True} 

96 

97 needs_2lat = { # 1 or 2 lattice parameters 

98 'fcc': False, 'bcc': False, 'sc': False, 

99 'hcp': True, 'graphite': True} 

100 

101 structure_factories = { 

102 'fcc': FaceCenteredCubic, 

103 'bcc': BodyCenteredCubic, 

104 'sc': SimpleCubic, 

105 'hcp': HexagonalClosedPacked, 

106 'graphite': Graphite} 

107 

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

109 import_names = { 

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

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

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

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

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

115 

116 # Default layer specifications for the different structures. 

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

118 ((1, 1, 0), 9), 

119 ((1, 1, 1), 5)], 

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

121 ((1, 1, 0), 9), 

122 ((1, 1, 1), 5)], 

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

124 ((1, 1, 0), 9), 

125 ((1, 1, 1), 5)], 

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

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

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

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

130 

131 def __init__(self, gui): 

132 self.atoms = None 

133 self.no_update = True 

134 self.old_structure = 'fcc' 

135 

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

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

138 

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

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

141 self.set_structure_data) 

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

143 win.add(self.element) 

144 win.add(self.elementinfo) 

145 win.add(lattice_button) 

146 

147 # The structure and lattice constant 

148 labels = [] 

149 values = [] 

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

151 labels.append(name) 

152 values.append(abbrev) 

153 self.structure_cb = ui.ComboBox( 

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

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

156 

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

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

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

160 

161 # Choose specification method 

162 self.method_cb = ui.ComboBox( 

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

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

165 callback=self.update_gui_method) 

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

167 

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

169 win.add(self.layerlabel) 

170 self.direction_table_rows = ui.Rows() 

171 win.add(self.direction_table_rows) 

172 self.default_direction_table() 

173 

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

175 self.new_direction_and_size_rows = ui.Rows() 

176 win.add(self.new_direction_and_size_rows) 

177 self.update_new_direction_and_size_stuff() 

178 

179 # Information 

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

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

182 ui.Label('-'), 

183 _(' Approx. diameter: '), 

184 ui.Label('-')] 

185 win.add(self.info) 

186 

187 # Finalize setup 

188 self.update_structure() 

189 self.update_gui_method() 

190 self.no_update = False 

191 

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

193 win.add(self.auto) 

194 

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

196 ui.helpbutton(helptext), 

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

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

199 

200 self.gui = gui 

201 self.smaller_button = None 

202 self.largeer_button = None 

203 

204 self.element.grab_focus() 

205 

206 def default_direction_table(self): 

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

208 self.direction_table = [] 

209 struct = self.structure_cb.value 

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

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

212 

213 def update_direction_table(self): 

214 self.direction_table_rows.clear() 

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

216 self.add_direction(direction, layers, energy) 

217 self.update() 

218 

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

220 i = len(self.direction_table_rows) 

221 

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

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

224 else: 

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

226 

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

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

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

230 

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

232 spin, up, down, delete]) 

233 up.active = i > 0 

234 down.active = False 

235 delete.active = i > 0 

236 

237 if i > 0: 

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

239 down.active = True 

240 delete.active = True 

241 

242 def update_new_direction_and_size_stuff(self): 

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

244 n = 4 

245 else: 

246 n = 3 

247 

248 rows = self.new_direction_and_size_rows 

249 

250 rows.clear() 

251 

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

253 for i in range(n): 

254 if i > 0: 

255 row.append(',') 

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

257 row.append('):') 

258 

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

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

261 else: 

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

263 

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

265 

266 rows.add(row) 

267 

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

269 # Extra widgets for the Wulff construction 

270 self.size_radio = ui.RadioButtons( 

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

272 ['natoms', 'diameter'], 

273 self.update_gui_size) 

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

275 self.update_size_natoms) 

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

277 self.update_size_diameter) 

278 self.round_radio = ui.RadioButtons( 

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

280 ['above', 'below', 'closest'], 

281 callback=self.update) 

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

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

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

285 rows.add(self.size_radio) 

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

287 _('ų'), self.size_diameter]) 

288 rows.add( 

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

290 rows.add(self.round_radio) 

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

292 self.update_gui_size() 

293 else: 

294 self.smaller_button = None 

295 self.larger_button = None 

296 

297 def update_structure(self, s=None): 

298 'Called when the user changes the structure.' 

299 s = self.structure_cb.value 

300 if s != self.old_structure: 

301 old4 = self.needs_4index[self.old_structure] 

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

303 # The table of directions is invalid. 

304 self.update_new_direction_and_size_stuff() 

305 self.default_direction_table() 

306 self.update_direction_table() 

307 self.old_structure = s 

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

309 self.update() 

310 

311 def update_gui_method(self, *args): 

312 'Switch between layer specification and Wulff construction.' 

313 self.update_direction_table() 

314 self.update_new_direction_and_size_stuff() 

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

316 self.layerlabel.text = _( 

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

318 else: 

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

320 

321 self.update() 

322 

323 def wulff_smaller(self, widget=None): 

324 'Make a smaller Wulff construction.' 

325 n = len(self.atoms) 

326 self.size_radio.value = 'natoms' 

327 self.size_natoms.value = n - 1 

328 self.round_radio.value = 'below' 

329 self.apply() 

330 

331 def wulff_larger(self, widget=None): 

332 'Make a larger Wulff construction.' 

333 n = len(self.atoms) 

334 self.size_radio.value = 'natoms' 

335 self.size_natoms.value = n + 1 

336 self.round_radio.value = 'above' 

337 self.apply() 

338 

339 def row_add(self, widget=None): 

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

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

342 n = 4 

343 else: 

344 n = 3 

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

346 if not any(idx): 

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

348 return 

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

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

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

352 return 

353 new = [idx, 5, 1.0] 

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

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

356 else: 

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

358 self.direction_table.append(new) 

359 self.add_direction(*new) 

360 self.update() 

361 

362 def row_delete(self, row): 

363 del self.direction_table[row] 

364 self.update_direction_table() 

365 

366 def row_swap_next(self, row): 

367 dt = self.direction_table 

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

369 self.update_direction_table() 

370 

371 def update_gui_size(self, widget=None): 

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

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

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

375 

376 def update_size_natoms(self, widget=None): 

377 at_vol = self.get_atomic_volume() 

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

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

380 self.size_diameter.value = dia 

381 self.update() 

382 

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

384 if self.size_diameter.active: 

385 at_vol = self.get_atomic_volume() 

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

387 self.size_natoms.value = int(n) 

388 if update: 

389 self.update() 

390 

391 def update(self, *args): 

392 if self.no_update: 

393 return 

394 self.element.Z # Check 

395 if self.auto.value: 

396 self.makeatoms() 

397 if self.atoms is not None: 

398 self.gui.new_atoms(self.atoms) 

399 else: 

400 self.clearatoms() 

401 self.makeinfo() 

402 

403 def set_structure_data(self, *args): 

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

405 z = self.element.Z 

406 if z is None: 

407 return 

408 ref = ase.data.reference_states[z] 

409 if ref is None: 

410 structure = None 

411 else: 

412 structure = ref['symmetry'] 

413 

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

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

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

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

418 return 

419 

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

421 

422 a = ref['a'] 

423 self.a.value = a 

424 if self.needs_4index[structure]: 

425 try: 

426 c = ref['c'] 

427 except KeyError: 

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

429 self.c.value = c 

430 

431 self.update_structure() 

432 

433 def makeatoms(self, *args): 

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

435 symbol = self.element.symbol 

436 if symbol is None: 

437 self.clearatoms() 

438 self.makeinfo() 

439 return False 

440 struct = self.structure_cb.value 

441 if self.needs_2lat[struct]: 

442 # a and c lattice constants 

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

444 'c': self.c.value} 

445 lc_str = str(lc) 

446 else: 

447 lc = self.a.value 

448 lc_str = f'{lc:.5f}' 

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

450 # Wulff construction 

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

452 surfaceenergies = [x[1].value 

453 for x in self.direction_table_rows.rows] 

454 self.update_size_diameter(update=False) 

455 rounding = self.round_radio.value 

456 self.atoms = wulff_construction(symbol, 

457 surfaces, 

458 surfaceenergies, 

459 self.size_natoms.value, 

460 self.structure_factories[struct], 

461 rounding, lc) 

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

463 'surfaces': str(surfaces), 

464 'energies': str(surfaceenergies), 

465 'latconst': lc_str, 

466 'natoms': self.size_natoms.value, 

467 'structure': struct, 

468 'rounding': rounding} 

469 else: 

470 # Layer-by-layer specification 

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

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

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

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

475 imp = self.import_names[struct] 

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

477 'element': symbol, 

478 'surfaces': str(surfaces), 

479 'layers': str(layers), 

480 'latconst': lc_str, 

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

482 self.makeinfo() 

483 

484 return python 

485 

486 def clearatoms(self): 

487 self.atoms = None 

488 

489 def get_atomic_volume(self): 

490 s = self.structure_cb.value 

491 a = self.a.value 

492 c = self.c.value 

493 if s == 'fcc': 

494 return a**3 / 4 

495 elif s == 'bcc': 

496 return a**3 / 2 

497 elif s == 'sc': 

498 return a**3 

499 elif s == 'hcp': 

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

501 elif s == 'graphite': 

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

503 

504 def makeinfo(self): 

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

506 

507 Also turns the Wulff construction buttons [Larger] and 

508 [Smaller] on and off. 

509 """ 

510 if self.atoms is None: 

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

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

513 else: 

514 at_vol = self.get_atomic_volume() 

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

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

517 self.info[3].text = f'{dia:.1f} Å' 

518 

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

520 if self.smaller_button is not None: 

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

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

523 

524 def apply(self, callbackarg=None): 

525 self.makeatoms() 

526 if self.atoms is not None: 

527 self.gui.new_atoms(self.atoms) 

528 return True 

529 else: 

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

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

532 'parameters.')) 

533 return False 

534 

535 def ok(self): 

536 if self.apply(): 

537 self.win.close()