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

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 _
6import numpy as np
8import ase
9import ase.data
10import ase.gui.ui as ui
12# Delayed imports:
13# ase.cluster.data
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
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""")
28helptext = _("""
29The nanoparticle module sets up a nano-particle or a cluster with a given
30crystal structure.
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.
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.
39How to specify the directions:
40------------------------------
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.
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""")
54py_template_layers = """
55import ase
56%(import)s
58surfaces = %(surfaces)s
59layers = %(layers)s
60lc = %(latconst)s
61atoms = %(factory)s('%(element)s', surfaces, layers, latticeconstant=lc)
63# OPTIONAL: Cast to ase.Atoms object, discarding extra information:
64# atoms = ase.Atoms(atoms)
65"""
67py_template_wulff = """
68import ase
69from ase.cluster import wulff_construction
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)
79# OPTIONAL: Cast to ase.Atoms object, discarding extra information:
80# atoms = ase.Atoms(atoms)
81"""
84class SetupNanoparticle:
85 "Window for setting up a nanoparticle."
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')}
94 needs_4index = { # 3 or 4 index dimension
95 'fcc': False, 'bcc': False, 'sc': False,
96 'hcp': True, 'graphite': True}
98 needs_2lat = { # 1 or 2 lattice parameters
99 'fcc': False, 'bcc': False, 'sc': False,
100 'hcp': True, 'graphite': True}
102 structure_factories = {
103 'fcc': FaceCenteredCubic,
104 'bcc': BodyCenteredCubic,
105 'sc': SimpleCubic,
106 'hcp': HexagonalClosedPacked,
107 'graphite': Graphite}
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'}
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)]}
132 def __init__(self, gui):
133 self.atoms = None
134 self.no_update = True
135 self.old_structure = 'fcc'
137 win = self.win = ui.Window(_('Nanoparticle'), wmtype='utility')
138 win.add(ui.Text(introtext))
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)
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])
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])
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])
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()
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()
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)
188 # Finalize setup
189 self.update_structure()
190 self.update_gui_method()
191 self.no_update = False
193 self.auto = ui.CheckButton(_('Automatic Apply'))
194 win.add(self.auto)
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)])
201 self.gui = gui
202 self.smaller_button = None
203 self.largeer_button = None
205 self.element.grab_focus()
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))
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()
220 def add_direction(self, direction, layers, energy):
221 i = len(self.direction_table_rows)
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)
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)
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
238 if i > 0:
239 down, delete = self.direction_table_rows[-2][3:]
240 down.active = True
241 delete.active = True
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
249 rows = self.new_direction_and_size_rows
251 rows.clear()
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('):')
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))
265 row.append(ui.Button(_('Add'), self.row_add))
267 rows.add(row)
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
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()
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:')
322 self.update()
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()
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()
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()
363 def row_delete(self, row):
364 del self.direction_table[row]
365 self.update_direction_table()
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()
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'
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()
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()
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()
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']
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
421 self.structure_cb.value = self.structure_names[structure]
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
432 self.update_structure()
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()
485 return python
487 def clearatoms(self):
488 self.atoms = None
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
505 def makeinfo(self):
506 """Fill in information field about the atoms.
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)
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
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
536 def ok(self):
537 if self.apply():
538 self.win.close()