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

291 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

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

4""" 

5from copy import copy 

6 

7import numpy as np 

8 

9import ase 

10import ase.data 

11import ase.gui.ui as ui 

12from ase.cluster import wulff_construction 

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

14from ase.cluster.hexagonal import Graphite, HexagonalClosedPacked 

15from ase.gui.i18n import _ 

16from ase.gui.widgets import Element, pybutton 

17 

18# Delayed imports: 

19# ase.cluster.data 

20 

21 

22introtext = _("""\ 

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

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

25specify the directions. 

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

27""") 

28 

29helptext = _(""" 

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

31crystal structure. 

32 

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

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

35 

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

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

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

39 

40How to specify the directions: 

41------------------------------ 

42 

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

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

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

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

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

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

49 

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

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

52the whole family of directions. 

53""") 

54 

55py_template_layers = """ 

56import ase 

57%(import)s 

58 

59surfaces = %(surfaces)s 

60layers = %(layers)s 

61lc = %(latconst)s 

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

63 

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

65# atoms = ase.Atoms(atoms) 

66""" 

67 

68py_template_wulff = """ 

69import ase 

70from ase.cluster import wulff_construction 

71 

72surfaces = %(surfaces)s 

73esurf = %(energies)s 

74lc = %(latconst)s 

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

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

77 size, '%(structure)s', 

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

79 

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

81# atoms = ase.Atoms(atoms) 

82""" 

83 

84 

85class SetupNanoparticle: 

86 "Window for setting up a nanoparticle." 

87 

88 structure_names = { 

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

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

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

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

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

94 

95 needs_4index = { # 3 or 4 index dimension 

96 'fcc': False, 'bcc': False, 'sc': False, 

97 'hcp': True, 'graphite': True} 

98 

99 needs_2lat = { # 1 or 2 lattice parameters 

100 'fcc': False, 'bcc': False, 'sc': False, 

101 'hcp': True, 'graphite': True} 

102 

103 structure_factories = { 

104 'fcc': FaceCenteredCubic, 

105 'bcc': BodyCenteredCubic, 

106 'sc': SimpleCubic, 

107 'hcp': HexagonalClosedPacked, 

108 'graphite': Graphite} 

109 

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

111 import_names = { 

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

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

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

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

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

117 

118 # Default layer specifications for the different structures. 

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

120 ((1, 1, 0), 9), 

121 ((1, 1, 1), 5)], 

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

123 ((1, 1, 0), 9), 

124 ((1, 1, 1), 5)], 

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

126 ((1, 1, 0), 9), 

127 ((1, 1, 1), 5)], 

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

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

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

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

132 

133 def __init__(self, gui): 

134 self.atoms = None 

135 self.no_update = True 

136 self.old_structure = 'fcc' 

137 

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

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

140 

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

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

143 self.set_structure_data) 

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

145 win.add(self.element) 

146 win.add(self.elementinfo) 

147 win.add(lattice_button) 

148 

149 # The structure and lattice constant 

150 labels = [] 

151 values = [] 

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

153 labels.append(name) 

154 values.append(abbrev) 

155 self.structure_cb = ui.ComboBox( 

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

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

158 

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

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

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

162 

163 # Choose specification method 

164 self.method_cb = ui.ComboBox( 

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

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

167 callback=self.update_gui_method) 

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

169 

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

171 win.add(self.layerlabel) 

172 self.direction_table_rows = ui.Rows() 

173 win.add(self.direction_table_rows) 

174 self.default_direction_table() 

175 

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

177 self.new_direction_and_size_rows = ui.Rows() 

178 win.add(self.new_direction_and_size_rows) 

179 self.update_new_direction_and_size_stuff() 

180 

181 # Information 

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

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

184 ui.Label('-'), 

185 _(' Approx. diameter: '), 

186 ui.Label('-')] 

187 win.add(self.info) 

188 

189 # Finalize setup 

190 self.update_structure() 

191 self.update_gui_method() 

192 self.no_update = False 

193 

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

195 win.add(self.auto) 

196 

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

198 ui.helpbutton(helptext), 

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

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

201 

202 self.gui = gui 

203 self.smaller_button = None 

204 self.largeer_button = None 

205 

206 self.element.grab_focus() 

207 

208 def default_direction_table(self): 

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

210 self.direction_table = [] 

211 struct = self.structure_cb.value 

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

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

214 

215 def update_direction_table(self): 

216 self.direction_table_rows.clear() 

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

218 self.add_direction(direction, layers, energy) 

219 self.update() 

220 

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

222 i = len(self.direction_table_rows) 

223 

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

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

226 else: 

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

228 

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

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

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

232 

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

234 spin, up, down, delete]) 

235 up.active = i > 0 

236 down.active = False 

237 delete.active = i > 0 

238 

239 if i > 0: 

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

241 down.active = True 

242 delete.active = True 

243 

244 def update_new_direction_and_size_stuff(self): 

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

246 n = 4 

247 else: 

248 n = 3 

249 

250 rows = self.new_direction_and_size_rows 

251 

252 rows.clear() 

253 

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

255 for i in range(n): 

256 if i > 0: 

257 row.append(',') 

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

259 row.append('):') 

260 

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

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

263 else: 

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

265 

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

267 

268 rows.add(row) 

269 

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

271 # Extra widgets for the Wulff construction 

272 self.size_radio = ui.RadioButtons( 

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

274 ['natoms', 'diameter'], 

275 self.update_gui_size) 

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

277 self.update_size_natoms) 

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

279 self.update_size_diameter) 

280 self.round_radio = ui.RadioButtons( 

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

282 ['above', 'below', 'closest'], 

283 callback=self.update) 

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

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

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

287 rows.add(self.size_radio) 

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

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

290 rows.add( 

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

292 rows.add(self.round_radio) 

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

294 self.update_gui_size() 

295 else: 

296 self.smaller_button = None 

297 self.larger_button = None 

298 

299 def update_structure(self, s=None): 

300 'Called when the user changes the structure.' 

301 s = self.structure_cb.value 

302 if s != self.old_structure: 

303 old4 = self.needs_4index[self.old_structure] 

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

305 # The table of directions is invalid. 

306 self.update_new_direction_and_size_stuff() 

307 self.default_direction_table() 

308 self.update_direction_table() 

309 self.old_structure = s 

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

311 self.update() 

312 

313 def update_gui_method(self, *args): 

314 'Switch between layer specification and Wulff construction.' 

315 self.update_direction_table() 

316 self.update_new_direction_and_size_stuff() 

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

318 self.layerlabel.text = _( 

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

320 else: 

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

322 

323 self.update() 

324 

325 def wulff_smaller(self, widget=None): 

326 'Make a smaller Wulff construction.' 

327 n = len(self.atoms) 

328 self.size_radio.value = 'natoms' 

329 self.size_natoms.value = n - 1 

330 self.round_radio.value = 'below' 

331 self.apply() 

332 

333 def wulff_larger(self, widget=None): 

334 'Make a larger Wulff construction.' 

335 n = len(self.atoms) 

336 self.size_radio.value = 'natoms' 

337 self.size_natoms.value = n + 1 

338 self.round_radio.value = 'above' 

339 self.apply() 

340 

341 def row_add(self, widget=None): 

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

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

344 n = 4 

345 else: 

346 n = 3 

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

348 if not any(idx): 

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

350 return 

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

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

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

354 return 

355 new = [idx, 5, 1.0] 

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

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

358 else: 

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

360 self.direction_table.append(new) 

361 self.add_direction(*new) 

362 self.update() 

363 

364 def row_delete(self, row): 

365 del self.direction_table[row] 

366 self.update_direction_table() 

367 

368 def row_swap_next(self, row): 

369 dt = self.direction_table 

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

371 self.update_direction_table() 

372 

373 def update_gui_size(self, widget=None): 

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

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

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

377 

378 def update_size_natoms(self, widget=None): 

379 at_vol = self.get_atomic_volume() 

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

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

382 self.size_diameter.value = dia 

383 self.update() 

384 

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

386 if self.size_diameter.active: 

387 at_vol = self.get_atomic_volume() 

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

389 self.size_natoms.value = int(n) 

390 if update: 

391 self.update() 

392 

393 def update(self, *args): 

394 if self.no_update: 

395 return 

396 self.element.Z # Check 

397 if self.auto.value: 

398 self.makeatoms() 

399 if self.atoms is not None: 

400 self.gui.new_atoms(self.atoms) 

401 else: 

402 self.clearatoms() 

403 self.makeinfo() 

404 

405 def set_structure_data(self, *args): 

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

407 z = self.element.Z 

408 if z is None: 

409 return 

410 ref = ase.data.reference_states[z] 

411 if ref is None: 

412 structure = None 

413 else: 

414 structure = ref['symmetry'] 

415 

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

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

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

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

420 return 

421 

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

423 

424 a = ref['a'] 

425 self.a.value = a 

426 if self.needs_4index[structure]: 

427 try: 

428 c = ref['c'] 

429 except KeyError: 

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

431 self.c.value = c 

432 

433 self.update_structure() 

434 

435 def makeatoms(self, *args): 

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

437 symbol = self.element.symbol 

438 if symbol is None: 

439 self.clearatoms() 

440 self.makeinfo() 

441 return False 

442 struct = self.structure_cb.value 

443 if self.needs_2lat[struct]: 

444 # a and c lattice constants 

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

446 'c': self.c.value} 

447 lc_str = str(lc) 

448 else: 

449 lc = self.a.value 

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

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

452 # Wulff construction 

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

454 surfaceenergies = [x[1].value 

455 for x in self.direction_table_rows.rows] 

456 self.update_size_diameter(update=False) 

457 rounding = self.round_radio.value 

458 self.atoms = wulff_construction(symbol, 

459 surfaces, 

460 surfaceenergies, 

461 self.size_natoms.value, 

462 self.structure_factories[struct], 

463 rounding, lc) 

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

465 'surfaces': str(surfaces), 

466 'energies': str(surfaceenergies), 

467 'latconst': lc_str, 

468 'natoms': self.size_natoms.value, 

469 'structure': struct, 

470 'rounding': rounding} 

471 else: 

472 # Layer-by-layer specification 

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

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

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

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

477 imp = self.import_names[struct] 

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

479 'element': symbol, 

480 'surfaces': str(surfaces), 

481 'layers': str(layers), 

482 'latconst': lc_str, 

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

484 self.makeinfo() 

485 

486 return python 

487 

488 def clearatoms(self): 

489 self.atoms = None 

490 

491 def get_atomic_volume(self): 

492 s = self.structure_cb.value 

493 a = self.a.value 

494 c = self.c.value 

495 if s == 'fcc': 

496 return a**3 / 4 

497 elif s == 'bcc': 

498 return a**3 / 2 

499 elif s == 'sc': 

500 return a**3 

501 elif s == 'hcp': 

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

503 elif s == 'graphite': 

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

505 

506 def makeinfo(self): 

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

508 

509 Also turns the Wulff construction buttons [Larger] and 

510 [Smaller] on and off. 

511 """ 

512 if self.atoms is None: 

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

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

515 else: 

516 at_vol = self.get_atomic_volume() 

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

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

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

520 

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

522 if self.smaller_button is not None: 

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

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

525 

526 def apply(self, callbackarg=None): 

527 self.makeatoms() 

528 if self.atoms is not None: 

529 self.gui.new_atoms(self.atoms) 

530 return True 

531 else: 

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

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

534 'parameters.')) 

535 return False 

536 

537 def ok(self): 

538 if self.apply(): 

539 self.win.close()