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."
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!
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'}
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)]}
123 def __init__(self, gui):
124 self.atoms = None
125 self.no_update = True
126 self.old_structure = 'undefined'
128 win = self.win = ui.Window(_('Nanoparticle'))
129 win.add(ui.Text(introtext))
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)
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]]
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])
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])
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()
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()
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)
185 # Finalize setup
186 self.update_structure('fcc')
187 self.update_gui_method()
188 self.no_update = False
190 self.auto = ui.CheckButton(_('Automatic Apply'))
191 win.add(self.auto)
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)])
198 self.gui = gui
199 self.smaller_button = None
200 self.largeer_button = None
202 self.element.grab_focus()
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))
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()
217 def add_direction(self, direction, layers, energy):
218 i = len(self.direction_table_rows)
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)
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)
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
235 if i > 0:
236 down, delete = self.direction_table_rows[-2][3:]
237 down.active = True
238 delete.active = True
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
246 rows = self.new_direction_and_size_rows
248 rows.clear()
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('):')
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))
262 row.append(ui.Button(_('Add'), self.row_add))
264 rows.add(row)
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
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]
307 self.update()
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:')
319 self.update()
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()
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()
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()
360 def row_delete(self, row):
361 del self.direction_table[row]
362 self.update_direction_table()
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()
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'
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()
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()
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()
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']
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
419 self.structure.value = structure
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
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()
483 return python
485 def clearatoms(self):
486 self.atoms = None
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
503 def makeinfo(self):
504 """Fill in information field about the atoms.
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)
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
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
534 def ok(self):
535 if self.apply():
536 self.win.close()