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
« 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
5import numpy as np
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
16# Delayed imports:
17# ase.cluster.data
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""")
27helptext = _("""
28The nanoparticle module sets up a nano-particle or a cluster with a given
29crystal structure.
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.
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.
38How to specify the directions:
39------------------------------
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.
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""")
53py_template_layers = """
54import ase
55%(import)s
57surfaces = %(surfaces)s
58layers = %(layers)s
59lc = %(latconst)s
60atoms = %(factory)s('%(element)s', surfaces, layers, latticeconstant=lc)
62# OPTIONAL: Cast to ase.Atoms object, discarding extra information:
63# atoms = ase.Atoms(atoms)
64"""
66py_template_wulff = """
67import ase
68from ase.cluster import wulff_construction
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)
78# OPTIONAL: Cast to ase.Atoms object, discarding extra information:
79# atoms = ase.Atoms(atoms)
80"""
83class SetupNanoparticle:
84 "Window for setting up a nanoparticle."
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')}
93 needs_4index = { # 3 or 4 index dimension
94 'fcc': False, 'bcc': False, 'sc': False,
95 'hcp': True, 'graphite': True}
97 needs_2lat = { # 1 or 2 lattice parameters
98 'fcc': False, 'bcc': False, 'sc': False,
99 'hcp': True, 'graphite': True}
101 structure_factories = {
102 'fcc': FaceCenteredCubic,
103 'bcc': BodyCenteredCubic,
104 'sc': SimpleCubic,
105 'hcp': HexagonalClosedPacked,
106 'graphite': Graphite}
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'}
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)]}
131 def __init__(self, gui):
132 self.atoms = None
133 self.no_update = True
134 self.old_structure = 'fcc'
136 win = self.win = ui.Window(_('Nanoparticle'), wmtype='utility')
137 win.add(ui.Text(introtext))
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)
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])
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])
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])
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()
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()
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)
187 # Finalize setup
188 self.update_structure()
189 self.update_gui_method()
190 self.no_update = False
192 self.auto = ui.CheckButton(_('Automatic Apply'))
193 win.add(self.auto)
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)])
200 self.gui = gui
201 self.smaller_button = None
202 self.largeer_button = None
204 self.element.grab_focus()
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))
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()
219 def add_direction(self, direction, layers, energy):
220 i = len(self.direction_table_rows)
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)
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)
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
237 if i > 0:
238 down, delete = self.direction_table_rows[-2][3:]
239 down.active = True
240 delete.active = True
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
248 rows = self.new_direction_and_size_rows
250 rows.clear()
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('):')
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))
264 row.append(ui.Button(_('Add'), self.row_add))
266 rows.add(row)
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
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()
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:')
321 self.update()
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()
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()
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()
362 def row_delete(self, row):
363 del self.direction_table[row]
364 self.update_direction_table()
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()
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'
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()
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()
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()
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']
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
420 self.structure_cb.value = self.structure_names[structure]
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
431 self.update_structure()
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()
484 return python
486 def clearatoms(self):
487 self.atoms = None
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
504 def makeinfo(self):
505 """Fill in information field about the atoms.
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} Å'
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
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
535 def ok(self):
536 if self.apply():
537 self.win.close()