diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..b68d1ee
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,20 @@
+# # Install pre-commit hooks via
+# pre-commit install
+
+- repo: local
+ hooks:
+ # yapf = yet another python formatter
+ - id: yapf
+ name: yapf
+ entry: yapf
+ language: system
+ types: [python]
+ args: ["-i"]
+
+ # prospector: collection of linters
+ - id: prospector
+ language: system
+ types: [file, python]
+ name: prospector
+ description: "This hook runs Prospector: https://github.com/landscapeio/prospector"
+ entry: prospector
diff --git a/.prospector.yaml b/.prospector.yaml
new file mode 100644
index 0000000..3067378
--- /dev/null
+++ b/.prospector.yaml
@@ -0,0 +1,19 @@
+max-line-length: 120
+
+ignore-paths:
+ - doc
+ - examples
+ - test
+ - utils
+
+pylint:
+ run: true
+
+pyflakes:
+ run: false
+
+pep8:
+ run: false
+
+mccabe:
+ run: false
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..87a4320
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,422 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint.
+jobs=1
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=bad-continuation,locally-disabled,useless-suppression,django-not-available,bad-option-value
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=
+
+
+[REPORTS]
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio).You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+
+[BASIC]
+
+# Naming hint for argument names
+argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct argument names
+argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Naming hint for attribute names
+attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct attribute names
+attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Naming hint for class attribute names
+class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Naming hint for class names
+class-name-hint=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression matching correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Naming hint for constant names
+const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression matching correct constant names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=5
+
+# Naming hint for function names
+function-name-hint=(([a-z][a-z0-9_]{2,40})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct function names
+function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_,pk
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Naming hint for inline iteration names
+inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Naming hint for method names
+method-name-hint=(([a-z][a-z0-9_]{2,40})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct method names
+method-rgx=(([a-z][a-z0-9_]{2,40})|(_[a-z0-9_]*)|(setUp)|(tearDown))$
+
+# Naming hint for module names
+module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression matching correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_,setUp,tearDown
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty
+
+# Naming hint for variable names
+variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct variable names
+variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=120
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,future.builtins
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+# This is a pylint issue https://github.com/PyCQA/pylint/issues/73
+ignored-modules=distutils
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,TERMIOS,Bastion,rexec
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=6
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=12
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of locals for function / method body
+max-locals=20
+
+# Maximum number of parents for a class (see R0901).
+max-parents=20
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=1
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/.style.yapf b/.style.yapf
new file mode 100644
index 0000000..b3d849f
--- /dev/null
+++ b/.style.yapf
@@ -0,0 +1,3 @@
+[style]
+based_on_style = google
+column_limit = 120
diff --git a/nanoribbon/viewers/__init__.py b/nanoribbon/viewers/__init__.py
index 7604873..798a171 100644
--- a/nanoribbon/viewers/__init__.py
+++ b/nanoribbon/viewers/__init__.py
@@ -1,5 +1,5 @@
"""Nanoribbon AiiDA lab viewers."""
-# pylint: disable=unused-import
+# pylint: disable=unused-import,wrong-import-position
from __future__ import absolute_import
from aiida import load_profile
@@ -8,4 +8,4 @@
from .search import NanoribbonSearchWidget
from .show_computed import NanoribbonShowWidget
from .pdos_computed import NanoribbonPDOSWidget
-from .smiles2GNR import Smiles2GNRWidget
\ No newline at end of file
+from .smiles2gnr import Smiles2GnrWidget
diff --git a/nanoribbon/viewers/search.py b/nanoribbon/viewers/search.py
index 3fea44d..de838bb 100644
--- a/nanoribbon/viewers/search.py
+++ b/nanoribbon/viewers/search.py
@@ -19,7 +19,7 @@ class NanoribbonSearchWidget(ipw.VBox):
STYLE = {"description_width":"120px"}
LAYOUT = ipw.Layout(width="80%")
- PREPROCESS_VERSION = 6.06
+ PREPROCESS_VERSION = 6.08
def __init__(self, **kwargs):
self.inp_pks = ipw.Text(description='PKs', placeholder='e.g. 4062 4753 (space separated)',
@@ -50,12 +50,12 @@ def slider(desc, min, max):
children = [self.inp_pks, self.inp_formula, self.text_description, self.inp_gap, self.inp_homo, self.inp_lumo,
self.inp_efermi, self.inp_tmagn, self.inp_amagn, self.button, self.results, self.info_out]
- super(NanoribbonSearchWidget, self).__init__(children, **kwargs)
+ super().__init__(children, **kwargs)
def on_click(self, change):
with self.info_out:
clear_output()
- self.search(do_all=False) #TODO: move to false, when done with the update
+ self.search(do_all=False) #INFO: move to False, when done with the update
def preprocess_workchains(self, do_all=False):
@@ -163,9 +163,16 @@ def get_calc_by_label(workcalc, label):
# vacuum level
export_hartree_calc = get_calc_by_label(workcalc, "export_hartree")
- fobj = StringIO(export_hartree_calc.outputs.retrieved.get_object_content("vacuum_hartree.dat"))
- data = np.loadtxt(fobj)
- vacuum_level = np.mean(data[:,2]) * HA2EV * 0.5
+ try:
+ fobj = StringIO(export_hartree_calc.outputs.retrieved.get_object_content("vacuum_hartree.dat"))
+ data = np.loadtxt(fobj)[:,2]
+ except FileNotFoundError:
+ try:
+ data = export_hartree_calc.outputs.output_data.get_array('data')
+ except KeyError:
+ raise Exception("Did not find 'vacuum_hartree.dat' file in the file repository or"
+ "'output_data' array in the output.")
+ vacuum_level = np.mean(data) * HA2EV * 0.5
workcalc.set_extra('vacuum_level', vacuum_level)
# store shifted energies
diff --git a/nanoribbon/viewers/show_computed.py b/nanoribbon/viewers/show_computed.py
index 91fd327..12d3ca6 100644
--- a/nanoribbon/viewers/show_computed.py
+++ b/nanoribbon/viewers/show_computed.py
@@ -1,505 +1,239 @@
+"""Viewers to display the results of the Nanoribbon work chain."""
+
+# Base imports.
import gzip
import re
import io
import tempfile
-
+from base64 import b64encode
+from collections import OrderedDict
import nglview
import ipywidgets as ipw
import bqplot as bq
import numpy as np
-from io import StringIO
-from base64 import b64encode
import matplotlib.pyplot as plt
from IPython.display import clear_output
-from collections import OrderedDict
import scipy.constants as const
import ase
-from ase.data import covalent_radii, atomic_numbers
-from ase.data.colors import cpk_colors
-from ase.neighborlist import NeighborList
-import ase.io
import ase.io.cube
-from aiida.orm import CalcJobNode, load_node, QueryBuilder, WorkChainNode
-
-on_band_click_global = None
-
-def read_cube(fn):
- lines = gzip.open(fn).read().decode('utf-8').splitlines()
- header = np.fromstring("".join(lines[2:6]), sep=' ').reshape(4,4)
- natoms, nx, ny, nz = header[:,0].astype(int)
- cube = dict()
- cube['x0'] = header[0,1] # x origin
- cube['y0'] = header[0,2] # y origin
- cube['z0'] = header[0,3] # z origin
- cube['dx'] = header[1,1] # x step size
- cube['dy'] = header[2,2] # y step size
- cube['dz'] = header[3,3] # z step size
- cube['data'] = np.fromstring(" ".join(lines[natoms+6:]), sep=' ').reshape(nx, ny, nz)
- return cube
-
-def plot_cube(ax, cube, z, cmap, vmin=-1, vmax=+1):
- assert cube['x0'] == 0.0 and cube['y0'] == 0.0
-
- a = np.flip(cube['data'][:,:,z].transpose(), axis=0)
- aa = np.tile(a, (1, 2))
- x2 = cube['dx'] * aa.shape[1] * 0.529177
- y2 = cube['dy'] * aa.shape[0] * 0.529177
-
- ax.set_xlabel(u'Å')
- ax.set_ylabel(u'Å')
- ax.set_xlim(0, x2)
- ax.set_ylim(0, y2)
-
- cax = ax.imshow(aa, extent=[0,x2,0,y2], cmap=cmap, vmin=vmin, vmax=vmax)
- return cax
-
-def plot_cuben(ax, cube_data,cube_atoms, z, cmap, vmin=-1, vmax=+1):
-
- a = np.flip(cube_data[:,:,z].transpose(), axis=0)
- aa = np.tile(a, (1, 2))
- x2 = cube_atoms.cell[0][0]*2.0#['dx'] * aa.shape[1] * 0.529177
- y2 = cube_atoms.cell[1][1]#['dy'] * aa.shape[0] * 0.529177
-
- ax.set_xlabel(u'Å')
- ax.set_ylabel(u'Å')
- ax.set_xlim(0, x2)
- ax.set_ylim(0, y2)
-
- cax = ax.imshow(aa, extent=[0,x2,0,y2], cmap=cmap, vmin=vmin, vmax=vmax)
- return cax
-
-def get_calc_by_label(workcalc, label):
- calcs = get_calcs_by_label(workcalc, label)
- assert len(calcs) == 1
- return calcs[0]
-
-def get_calcs_by_label(workcalc, label):
- qb = QueryBuilder()
- qb.append(WorkChainNode, filters={'uuid':workcalc.uuid})
- qb.append(CalcJobNode, with_incoming=WorkChainNode, filters={'label':label})
- calcs = [ c[0] for c in qb.all() ]
- for calc in calcs:
- assert(calc.is_finished_ok == True)
- return calcs
-
-def set_cube_isosurf(isovals, colors, ngl_viewer):
- if hasattr(ngl_viewer, 'component_1'):
- c2 = ngl_viewer.component_1
- c2.clear()
- for isov, col in zip(isovals, colors):
- c2.add_surface(color=col, isolevelType="value", isolevel=isov)
-
-def setup_cube_plot(file_name, ngl_viewer):
-
- data, atoms = ase.io.cube.read_cube_data(file_name)
- atoms.pbc=True
-
- ## -------------------------------
- ## In case of single unit cell
- #with gzip.open(file_name, 'rb') as fh:
- # file_data = fh.read()
- # file_obj = io.StringIO(file_data.decode('UTF-8'))
- #c1 = ngl_viewer.add_component(nglview.ASEStructure(atoms))
- #c2 = ngl_viewer.add_component(file_obj, ext='cube')
- ## -------------------------------
- ## For multiple unit cells (might be considerably slower)
- n_repeat = 2
- atoms_xn = atoms.repeat((n_repeat,1,1))
- data_xn = np.tile(data, (n_repeat,1,1))
- c1 = ngl_viewer.add_component(nglview.ASEStructure(atoms_xn))
- with tempfile.NamedTemporaryFile(mode='w') as tempf:
- ase.io.cube.write_cube(tempf, atoms_xn, data_xn)
- c2 = ngl_viewer.add_component(tempf.name, ext='cube')
- c2.clear()
- ## -------------------------------
-
-
-class NanoribbonShowWidget(ipw.HBox):
- def __init__(self, workcalc, **kwargs):
- self._workcalc = workcalc
+from traitlets import dlink, observe, Instance, Int
+
+# AiiDA imports.
+from aiida.common import exceptions
+from aiida.plugins import DataFactory
+
+# Local imports.
+from .utils import plot_struct_2d, get_calc_by_label, get_calcs_by_label, from_cube_to_arraydata
+
+# AiiDA data objects.
+ArrayData = DataFactory("array") # pylint: disable=invalid-name
+BandsData = DataFactory("array.bands") # pylint: disable=invalid-name
+StructureData = DataFactory("structure") # pylint: disable=invalid-name
+
+ANG_2_BOHR = 1.889725989
+
+NANORIBBON_INFO = """
+WorkCalculation PK: {pk}
+Total energy (eV): {energy}
+Band gap (eV): {gap}
+Total magnetization/A: {totmagn}
+Absolute magnetization/A: {absmagn}
+"""
+
+
+class BandsViewerWidget(ipw.VBox):
+ """Widget to view AiiDA BandsData object."""
+ bands = Instance(BandsData, allow_none=True)
+ structure = Instance(StructureData, allow_none=True)
+ selected_band = Int(allow_none=True)
+ selected_kpoint = Int(allow_none=True)
+
+ def __init__(self, **kwargs):
+ self.bands = kwargs['bands']
+ self.structure = kwargs['structure']
+ self.vacuum_level = kwargs['vacuum_level']
+ self.bands_array = self.bands.get_bands()
+ self.band_plots = []
+ self.homo = kwargs['homo']
+ self.lumo = kwargs['lumo']
- print("WorkCalculation PK: {}".format(workcalc.id))
- print("total energy: {} eV".format(workcalc.extras['total_energy']))
- print("gap: {} eV".format(workcalc.extras['gap']))
- print("total magnetization/A: {}".format(workcalc.extras['absolute_magnetization_per_angstr']))
- print("abs. magnetization/A: {}".format(workcalc.extras['total_magnetization_per_angstr']))
+ # Always make the array 3-dimensional.
+ if self.bands_array.ndim == 2:
+ self.bands_array = self.bands_array[None, :, :]
+ self.eff_mass_parabolas = []
- self.orbitals_calcs = get_calcs_by_label(workcalc, "export_orbitals")
- bands_calc = get_calc_by_label(workcalc, "bands")
- self.structure = bands_calc.inputs.structure
- self.ase_struct = self.structure.get_ase()
- self.selected_cube_file = None
- self.selected_cube = None
- self.selected_data = None
- self.selected_spin = None
- self.selected_band = None
- self.bands = bands_calc.outputs.output_band.get_bands()
- self.vbm=int(bands_calc.outputs.output_parameters.get_dict()['number_of_electrons']/2)
- if self.bands.ndim == 2:
- self.bands = self.bands[None,:,:]
+ layout = ipw.Layout(padding="5px", margin="0px")
- self.band_plots = []
- boxes = []
- self.eff_mass_parabolas = []
- for ispin in range(self.bands.shape[0]):
+ # Slider to control how many points of the band to use for parabolic fit.
+ self.efm_fit_slider = ipw.IntSlider(description="Eff. mass fit",
+ min=3,
+ max=15,
+ step=2,
+ continuous_update=False,
+ layout=layout)
+ band_selector = ipw.IntSlider(description="Band",
+ value=int(kwargs['nelectrons'] / 2) - 1,
+ min=0,
+ max=self.bands_array.shape[2],
+ step=1,
+ continuous_update=False,
+ layout=layout)
+ kpoint_slider = ipw.IntSlider(description="k-point",
+ min=1,
+ max=self.bands_array.shape[1],
+ continuous_update=False,
+ layout=layout)
+ self.spin_selector = ipw.RadioButtons(options=[('up', 0), ('down', 1)],
+ description='Select spin',
+ disabled=False)
+
+ boxes = [self.efm_fit_slider, band_selector, kpoint_slider, self.spin_selector]
+
+ plots = []
+ for ispin in range(self.bands_array.shape[0]):
box, plot, eff_mass_parabola = self.plot_bands(ispin)
- boxes.append(box)
+ plots.append(box)
self.band_plots.append(plot)
self.eff_mass_parabolas.append(eff_mass_parabola)
+ boxes.append(ipw.HBox(plots))
- layout = ipw.Layout(padding="5px", margin="0px")
- self.info_out = ipw.Output(layout=layout)
- self.kpnt_out = ipw.Output(layout=layout)
- self.orb_out = ipw.Output(layout=layout)
-
- layout = ipw.Layout(width="400px")
-
- ### -----------------------------
- ### Slider to control how many points of the band to use for parabolic fit
-
- # Odd values of fit have better accuracy, so it's worth it to disable even values
- self.efm_fit_slider = ipw.IntSlider(description="eff. mass fit", min=3, max=15, step=2, continuous_update=False, layout=layout)
- # Only if a band is selected, selecting a new effective mass fit will update the plot and infopanel
- on_efm_fit_change = lambda c: self.on_band_change() if self.selected_spin else None
- self.efm_fit_slider.observe(on_efm_fit_change, names='value')
- ### -----------------------------
-
-
-
-
- self.kpoint_slider = ipw.IntSlider(description="k-point", min=1, max=1, continuous_update=False, layout=layout)
- self.kpoint_slider.observe(self.on_kpoint_change, names='value')
-
-
-
-
- self.height_slider = ipw.SelectionSlider(description="height", options={"---":0}, continuous_update=False, layout=layout)
- self.height_slider.observe(self.on_orb_plot_change, names='value')
-
- self.orb_alpha_slider = ipw.FloatSlider(description="opacity", value=0.5, max=1.0, continuous_update=False, layout=layout)
- self.orb_alpha_slider.observe(self.on_orb_plot_change, names='value')
-
- #self.colormap_slider = ipw.FloatRangeSlider(description='colormap',min=-4, max=-1, step=0.5,
- # value=[-4, -2], continuous_update=False, readout_format='.1f', layout=layout)
- self.colormap_slider = ipw.FloatLogSlider(value=0.01,base=10,min=-4, max=-1, step=0.5,
- description='Color max',readout_format='.1e', continuous_update=False, layout=layout)
- self.colormap_slider.observe(self.on_orb_plot_change, names='value')
-
- ### TEMPORARY FIX FOR BAND CLICK NOT WORKING
- #self.kpt_tmp = ipw.BoundedIntText(value=1, min=1,max=12,step=1,description='kpt:',disabled=False)
- #self.kpt_tmp.observe(self.on_kpoint_change, names='value')
-#if no band click self.bnd_tmp = ipw.BoundedIntText(value=0, min=-4,max=4,step=1,description='band 0=homo',disabled=False)
- #self.bnd_tmp.observe(self.on_band_change(selected_spin=0, selected_band=self.bnd_tmp.value + self.vbm -1), names='value')
- on_band_change_lambda = lambda c: self.on_band_change(selected_spin=0, selected_band=c['new'])
-#if no band click self.bnd_tmp.observe(on_band_change_lambda, names='value')
- ### END TEMPORARY FIX
-
- # -----------------------
- # Orbital 3d visualization
- self.orbital_ngl = nglview.NGLWidget()
- self.orb_isosurf_slider = ipw.FloatSlider(continuous_update=False,
- value=1e-3,
- min=1e-4,
- max=1e-2,
- step=1e-4,
- description='isovalue',
- readout_format='.1e'
- )
- self.orb_isosurf_slider.observe(lambda c: set_cube_isosurf([c['new']], ['red'], self.orbital_ngl), names='value')
- self.orbital_3d_box = ipw.VBox([self.orbital_ngl, self.orb_isosurf_slider])
- # -----------------------
-
- # Display the orbital map also initially
- self.on_band_change(selected_spin=0, selected_band=self.vbm -1)
-
- layout = ipw.Layout(align_items="center")
- side_box = ipw.VBox([self.info_out,
- self.efm_fit_slider,
- self.kpoint_slider,
- #self.bnd_tmp,
- self.height_slider,
- self.orb_alpha_slider,
- self.colormap_slider,
- self.kpnt_out,
- ipw.HBox([self.orb_out, self.orbital_3d_box])], layout=layout)
- boxes.append(side_box)
- super(NanoribbonShowWidget, self).__init__(boxes, **kwargs)
+ dlink((kpoint_slider, 'value'), (self, 'selected_kpoint'))
+ dlink((band_selector, 'value'), (self, 'selected_band'))
+
+ # Display the orbital map also initially.
+ self.on_band_change(_=None)
+
+ super().__init__(boxes, **kwargs)
def plot_bands(self, ispin):
- global on_band_click_global
- _, nkpoints, nbands = self.bands.shape
- homo = self._workcalc.get_extra('homo')
- lumo = self._workcalc.get_extra('lumo')
-
- center = (homo + lumo) / 2.0
+ """Plot band structure."""
+ _, nkpoints, nbands = self.bands_array.shape
+ center = (self.homo + self.lumo) / 2.0
x_sc = bq.LinearScale()
- y_sc = bq.LinearScale(min=center-3.0, max=center+3.0, )
+ y_sc = bq.LinearScale(
+ min=center - 3.0,
+ max=center + 3.0,
+ )
- color_sc = bq.ColorScale(colors=['gray', 'red'], min=0.0, max=1.0)
- colors = np.zeros(nbands)
-
- Lx = self.structure.cell_lengths[0]
- x_max = np.pi / Lx
- ax_x = bq.Axis(label=u'kA^-1', scale=x_sc, grid_lines='solid', tick_format='.3f', tick_values=[0, x_max]) #, tick_values=[0.0, 0.5])
- ax_y = bq.Axis(label='eV', scale=y_sc, orientation='vertical', grid_lines='solid')
+ x_max = np.pi / self.structure.cell_lengths[0]
x_data = np.linspace(0.0, x_max, nkpoints)
- y_datas = self.bands[ispin,:,:].transpose() - self._workcalc.get_extra('vacuum_level')
-
- lines = bq.Lines(x=x_data, y=y_datas, color=colors, animate=True, stroke_width=4.0, scales={'x': x_sc, 'y': y_sc, 'color': color_sc})
-
- homo_line = bq.Lines(x=[0, x_max], y=[homo, homo], line_style='dashed', colors=['red'], scales={'x': x_sc, 'y': y_sc})
+ y_datas = self.bands_array[ispin, :, :].transpose() - self.vacuum_level
+
+ lines = bq.Lines(x=x_data,
+ y=y_datas,
+ color=np.zeros(nbands),
+ animate=True,
+ stroke_width=4.0,
+ scales={
+ 'x': x_sc,
+ 'y': y_sc,
+ 'color': bq.ColorScale(colors=['gray', 'red'], min=0.0, max=1.0)
+ })
+
+ homo_line = bq.Lines(x=[0, x_max],
+ y=[self.homo, self.homo],
+ line_style='dashed',
+ colors=['red'],
+ scales={
+ 'x': x_sc,
+ 'y': y_sc
+ })
# Initialize the parabola as a random line and set visible to false
# Later, when it is correctly set, show it.
- eff_mass_parabola = bq.Lines(x=[0, 0], y=[0, 0], visible=False, stroke_width=1.0,
- line_style='solid', colors=['blue'], scales={'x': x_sc, 'y': y_sc})
+ eff_mass_parabola = bq.Lines(x=[0, 0],
+ y=[0, 0],
+ visible=False,
+ stroke_width=1.0,
+ line_style='solid',
+ colors=['blue'],
+ scales={
+ 'x': x_sc,
+ 'y': y_sc
+ })
+ ax_x = bq.Axis(label=u'kA^-1', scale=x_sc, grid_lines='solid', tick_format='.3f', tick_values=[0, x_max])
+ ax_y = bq.Axis(label='eV', scale=y_sc, orientation='vertical', grid_lines='solid')
- ratio = 0.25
-
- fig = bq.Figure(axes=[ax_x, ax_y], marks=[lines, homo_line, eff_mass_parabola], title='Spin {}'.format(ispin),
+ fig = bq.Figure(axes=[ax_x, ax_y],
+ marks=[lines, homo_line, eff_mass_parabola],
+ title='Spin {}'.format(ispin),
layout=ipw.Layout(height="800px", width="200px"),
- fig_margin={"left": 45, "top": 60, "bottom":60, "right":40},
- min_aspect_ratio=ratio, max_aspect_ratio=ratio)
-
- on_band_click_global = self.on_band_change
- def on_band_click(self, target):
- #with self.info_out:
- # print("CLICKED")
- global on_band_click_global
- #self.selected_spin = ispin
- #self.selected_band = target['data']['index']
- on_band_click_global(ispin, target['data']['index'])
-
- lines.on_element_click(on_band_click)
+ fig_margin={
+ "left": 45,
+ "top": 60,
+ "bottom": 60,
+ "right": 40
+ },
+ min_aspect_ratio=0.25,
+ max_aspect_ratio=0.25)
save_btn = ipw.Button(description="Download png")
- save_btn.on_click(lambda b: fig.save_png()) # save_png() does not work with unicode labels
-
- igor_link = self.mk_igor_link(ispin)
+ save_btn.on_click(lambda b: fig.save_png()) # save_png() does not work with unicode labels
- box = ipw.VBox([fig, save_btn, igor_link],
+ box = ipw.VBox([fig, save_btn, self.mk_igor_link(ispin)],
layout=ipw.Layout(align_items="center", padding="5px", margin="0px"))
return box, lines, eff_mass_parabola
def mk_igor_link(self, ispin):
+ """Create a downloadable link."""
igorvalue = self.igor_bands(ispin)
igorfile = b64encode(igorvalue.encode())
- filename = self.ase_struct.get_chemical_formula() + "_bands_spin{}_pk{}.itx".format(ispin, self.structure.id)
+ filename = self.structure.get_ase().get_chemical_formula() + "_bands_spin{}_pk{}.itx".format(
+ ispin, self.structure.id)
html = 'Export itx-Bands'
-
return ipw.HTML(html)
def igor_bands(self, ispin):
- _, nkpoints, nbands = self.bands.shape
+ """Exprot the band structure in IGOR data format."""
+ _, nkpoints, nbands = self.bands_array.shape
k_axis = np.linspace(0.0, np.pi / self.structure.cell_lengths[0], nkpoints)
- testio = StringIO()
- tosave = self.bands[ispin,:,:].transpose() - self._workcalc.get_extra('vacuum_level')
-
- with testio as f:
- f.write(u'IGOR\r')
- f.write(u'WAVES')
- f.write(u'\tx1'+(u'\ty{}'*nbands).format(*[x for x in range(nbands)])+u'\r')
- f.write(u'BEGIN\r')
+ testio = io.StringIO()
+ tosave = self.bands_array[ispin, :, :].transpose() - self.vacuum_level
+
+ with testio as fobj:
+ fobj.write(u'IGOR\r')
+ fobj.write(u'WAVES')
+ fobj.write(u'\tx1' + (u'\ty{}' * nbands).format(*[x for x in range(nbands)]) + u'\r')
+ fobj.write(u'BEGIN\r')
for i in range(nkpoints):
- f.write(u"\t{:.7f}".format(k_axis[i])) # first column k_axis
- f.write((u"\t{:.7f}"*nbands).format(*tosave[:,i])) # other columns the bands
- f.write(u"\r")
- f.write(u"END\r")
- f.write(u'X SetScale/P x {},{},"", x1; SetScale y 0,0,"", x1\r'.format(0, k_axis[1]-k_axis[0]))
+ fobj.write(u"\t{:.7f}".format(k_axis[i])) # first column k_axis
+ fobj.write((u"\t{:.7f}" * nbands).format(*tosave[:, i])) # other columns the bands
+ fobj.write(u"\r")
+ fobj.write(u"END\r")
+ fobj.write(u'X SetScale/P x {},{},"", x1; SetScale y 0,0,"", x1\r'.format(0, k_axis[1] - k_axis[0]))
for idk in range(nbands):
- f.write((u'X SetScale/P x 0,1,"", y{0}; SetScale y 0,0,"", y{0}\r').format(str(idk)))
+ fobj.write((u'X SetScale/P x 0,1,"", y{0}; SetScale y 0,0,"", y{0}\r').format(str(idk)))
return testio.getvalue()
- def on_band_change(self, selected_spin=None, selected_band=None):
- self.selected_spin = selected_spin
- self.selected_band = selected_band
- nspins, _, nbands = self.bands.shape
+ @observe('selected_band')
+ def on_band_change(self, _=None):
+ """Highlight the selected band."""
+ self.selected_spin = self.spin_selector.value
+ nspins, _, nbands = self.bands_array.shape
- with self.info_out:
- clear_output()
- print("selected spin: {}".format(self.selected_spin))
- print("selected band: {}".format(self.selected_band))
-
- colors = np.zeros((nspins, nbands))
- colors[self.selected_spin, self.selected_band] = 1.0
- for ispin in range(nspins):
- self.band_plots[ispin].color = colors[ispin,:]
-
- # orbitals_calcs might use fewer nkpoints than bands_calc
- print(self.orbitals_calcs[0].inputs)
- prev_calc = self.orbitals_calcs[0].inputs.parent_folder.creator
- nkpoints_lowres = prev_calc.res.number_of_k_points
-
- print(nkpoints_lowres, self.selected_spin)
- lower = nkpoints_lowres * self.selected_spin
- upper = lower + nkpoints_lowres
- self.selected_cube_files = []
- for fn in sorted([ fdr.name for orbitals_calc in self.orbitals_calcs
- for fdr in orbitals_calc.outputs.retrieved.list_objects()]):
- m = re.match("aiida.filplot_K(\d\d\d)_B(\d\d\d)_orbital.cube.gz", fn)
- if not m:
- continue
- k, b = int(m.group(1)), int(m.group(2))
- if b != self.selected_band + 1:
- continue
- if lower < k and k <= upper:
- self.selected_cube_files.append(fn)
-
- n = len(self.selected_cube_files)
- self.kpoint_slider.max = max(n, 1)
- print("found {} cube files".format(n))
- self.on_kpoint_change(None)
-
- ### -------------------------------------------
- ### Effective mass calculation and parabola plotting
-
- meff, parabola_fit, fit_kvals, fit_energies = self.calc_effective_mass(ispin=self.selected_spin)
- print("effective mass: %f"%meff)
+ colors = np.zeros((nspins, nbands))
+ colors[self.selected_spin, self.selected_band] = 1.0
- parab_k_arr = np.linspace(np.min(fit_kvals), np.max(fit_kvals), 20)
- parab_e_arr = parabola_fit[0]*parab_k_arr**2 + parabola_fit[1]*parab_k_arr + parabola_fit[2]
- self.eff_mass_parabolas[self.selected_spin].x = parab_k_arr
- self.eff_mass_parabolas[self.selected_spin].y = parab_e_arr
- self.eff_mass_parabolas[self.selected_spin].visible = True
-
- if nspins > 1:
- self.eff_mass_parabolas[(self.selected_spin+1)%2].visible = False
-
- ### -------------------------------------------
- def on_kpoint_change(self, c):
- with self.kpnt_out:
- clear_output()
- i = self.kpoint_slider.value
- if i > len(self.selected_cube_files):
- print("Found no cube files")
- #self.selected_cube = None
- self.selected_cube_file = None
- self.selected_data = None
- self.height_slider.options = {"---":0}
-
- else:
- fn = self.selected_cube_files[i-1]
- for orbitals_calc in self.orbitals_calcs:
- try:
- absfn = orbitals_calc.outputs.retrieved.open(fn).name
- except FileNotFoundError:
- continue
-
- #self.selected_cube = read_cube(absfn)
- #nz = self.selected_cube['data'].shape[2]
- #z0 = self.selected_cube['z0']
- self.selected_cube_file = absfn
- self.selected_data, self.selected_atoms = ase.io.cube.read_cube_data(absfn)
- nz = self.selected_data.shape[2]
- dz=self.selected_atoms.cell[2][2] / nz
- #origin of cube file assumed in 0,0,0...
- #dz = self.selected_cube['dz']
-
- #zmid = self.structure.cell_lengths[2] / 2.0
- zmid=self.selected_atoms.cell[2][2] / 2.0
- options = OrderedDict()
- #for i in range(nz):
- # z = (z0 + dz*i) * 0.529177 - zmid
- # options[u"{:.3f} Å".format(z)] = i
- for i in range(0,nz,3):
- z = dz*i
- options[u"{:.3f} Å".format(z)] = i
- self.height_slider.options = options
- nopt=int(len(options)/2)
- self.height_slider.value = list(options.values())[nopt+1]
-
- # Plot 2d
- self.on_orb_plot_change(None)
-
- # Plot 3d
- self.orbital_3d()
- break
-
- self.on_orb_plot_change(None)
-
- def on_orb_plot_change(self, c):
- with self.orb_out:
- clear_output()
- #if self.selected_cube is None:
- if self.selected_data is None:
- return
-
- fig, ax = plt.subplots()
- fig.dpi = 150.0
- #vmin = 10 ** self.colormap_slider.value[0]
- #vmax = 10 ** self.colormap_slider.value[1]
- vmin = self.colormap_slider.value/100.0
- vmax = self.colormap_slider.value
-
- #cax = plot_cube(ax, self.selected_cube, self.height_slider.value, 'gray', vmin, vmax)
- cax = plot_cuben(ax, self.selected_data,self.selected_atoms, self.height_slider.value, 'seismic',vmin,vmax)
- fig.colorbar(cax, label='e/bohr^3', ticks=[vmin, vmax], format='%.0e', orientation='horizontal', shrink=0.3)
-
- self.plot_overlay_structn(ax,self.selected_atoms, self.orb_alpha_slider.value)
- plt.show()
-
- def plot_overlay_struct(self, ax, alpha):
- if alpha == 0:
- return
-
- # plot overlayed structure
- s = self.ase_struct.repeat((2,1,1))
- cov_radii = [covalent_radii[a.number] for a in s]
- nl = NeighborList(cov_radii, bothways = True, self_interaction = False)
- nl.update(s)
-
- for at in s:
- #circles
- x, y, z = at.position
- n = atomic_numbers[at.symbol]
- ax.add_artist(plt.Circle((x,y), covalent_radii[n]*0.5, color=cpk_colors[n], fill=True, clip_on=True, alpha=alpha))
- #bonds
- nlist = nl.get_neighbors(at.index)[0]
- for theneig in nlist:
- x,y,z = (s[theneig].position + at.position)/2
- x0,y0,z0 = at.position
- if (x-x0)**2 + (y-y0)**2 < 2 :
- ax.plot([x0,x],[y0,y],color=cpk_colors[n],linewidth=2,linestyle='-', alpha=alpha)
-
- def plot_overlay_structn(self, ax,atoms, alpha):
- if alpha == 0:
- return
-
- # plot overlayed structure
- s = atoms.repeat((2,1,1))
- cov_radii = [covalent_radii[a.number] for a in s]
- nl = NeighborList(cov_radii, bothways = True, self_interaction = False)
- nl.update(s)
-
- for at in s:
- #circles
- x, y, z = at.position
- n = atomic_numbers[at.symbol]
- ax.add_artist(plt.Circle((x,y), covalent_radii[n]*0.5, color=cpk_colors[n], fill=True, clip_on=True, alpha=alpha))
- #bonds
- nlist = nl.get_neighbors(at.index)[0]
- for theneig in nlist:
- x,y,z = (s[theneig].position + at.position)/2
- x0,y0,z0 = at.position
- if (x-x0)**2 + (y-y0)**2 < 2 :
- ax.plot([x0,x],[y0,y],color=cpk_colors[n],linewidth=2,linestyle='-', alpha=alpha)
+ for ispin in range(nspins):
+ self.band_plots[ispin].color = colors[ispin, :]
def calc_effective_mass(self, ispin):
+ """Compute effective mass."""
# m* = hbar^2*[d^2E/dk^2]^-1
hbar = const.value('Planck constant over 2 pi in eV s')
- el_mass = const.m_e*1e-20/const.eV # in eV*s^2/ang^2
- _, nkpoints, _ = self.bands.shape
- band = self.bands[ispin].transpose()[self.selected_band] - self._workcalc.get_extra('vacuum_level')
+ el_mass = const.m_e * 1e-20 / const.eV # in eV*s^2/ang^2
+ _, nkpoints, _ = self.bands_array.shape
+ band = self.bands_array[ispin].transpose()[self.selected_band] - self.vacuum_level
k_axis = np.linspace(0.0, np.pi / self.structure.cell_lengths[0], nkpoints)
num_fit_points = self.efm_fit_slider.value
- if np.amax(band) >= self._workcalc.get_extra('lumo'):
+ if np.amax(band) >= self.lumo:
# conduction band, let's search for effective electron mass (lowest point in energy)
parabola_ind = np.argmin(band)
else:
@@ -511,19 +245,15 @@ def calc_effective_mass(self, ispin):
k_vals_ext = np.concatenate([-np.flip(k_axis, 0)[:-1], k_axis, k_axis[-1] + k_axis[1:]])
# define fitting region
- i_min = parabola_ind - int(np.ceil(num_fit_points/2.0)) + len(band)
- i_max = parabola_ind + int(np.floor(num_fit_points/2.0)) + len(band)
+ i_min = parabola_ind - int(np.ceil(num_fit_points / 2.0)) + len(band)
+ i_max = parabola_ind + int(np.floor(num_fit_points / 2.0)) + len(band)
fit_energies = band_ext[i_min:i_max]
fit_kvals = k_vals_ext[i_min:i_max]
- #print(k_axis[parabola_ind], band[parabola_ind])
- #print(fit_kvals)
- #print(fit_energies)
-
parabola_fit = np.polyfit(fit_kvals, fit_energies, 2)
- meff = hbar**2/(2*parabola_fit[0])/el_mass
+ meff = hbar**2 / (2 * parabola_fit[0]) / el_mass
# restrict fitting values to "main region"
main_region_mask = (fit_kvals >= k_axis[0]) & (fit_kvals <= k_axis[-1])
@@ -532,76 +262,308 @@ def calc_effective_mass(self, ispin):
return meff, parabola_fit, fit_kvals, fit_energies
+
+class CubeArrayData3dViewerWidget(ipw.VBox):
+ """Widget to View 3-dimensional AiiDA ArrayData object in 3D."""
+
+ arraydata = Instance(ArrayData, allow_none=True)
+
+ def __init__(self, **kwargs):
+ self.data_3d = None
+ self.units = None
+ self.structure = None
+ self.viewer = nglview.NGLWidget()
+ self.orb_isosurf_slider = ipw.FloatSlider(continuous_update=False,
+ value=1e-3,
+ min=1e-4,
+ max=1e-2,
+ step=1e-4,
+ description='Isovalue',
+ readout_format='.1e')
+ self.orb_isosurf_slider.observe(
+ lambda c: self.set_cube_isosurf( # pylint: disable=no-member
+ [c['new'], -c['new']], ['red', 'blue']),
+ names='value')
+ super().__init__([self.viewer, self.orb_isosurf_slider], **kwargs)
+
+ @observe('arraydata')
+ def on_observe_arraydata(self, _=None):
+ """Update object attributes when arraydata trait is modified."""
+
+ self.data_3d = self.arraydata.get_array('data')
+ cell = self.arraydata.get_array('voxel') / ANG_2_BOHR
+ for i in range(3):
+ cell[i, :] *= self.data_3d.shape[i]
+ self.structure = ase.Atoms(
+ numbers=self.arraydata.get_array('atomic_numbers'),
+ positions=self.arraydata.get_array('coordinates') / ANG_2_BOHR,
+ cell=cell,
+ pbc=True,
+ )
+ self.units = self.arraydata.get_array('data_units')
+ self.update_plot()
+
+ def update_plot(self):
+ """Update the 3D plot."""
+ # pylint: disable=no-member
+ while hasattr(self.viewer, "component_0"):
+ self.viewer.component_0.clear_representations()
+ self.viewer.remove_component(self.viewer.component_0.id)
+ self.setup_cube_plot()
+ self.set_cube_isosurf(
+ [self.orb_isosurf_slider.value, -self.orb_isosurf_slider.value], # pylint: disable=invalid-unary-operand-type
+ ['red', 'blue'])
+
+ def setup_cube_plot(self):
+ """Setup cube plot."""
+ # pylint: disable=no-member
+ n_repeat = 2
+ atoms_xn = self.structure.repeat((n_repeat, 1, 1))
+ data_xn = np.tile(self.data_3d, (n_repeat, 1, 1))
+ self.viewer.add_component(nglview.ASEStructure(atoms_xn))
+ with tempfile.NamedTemporaryFile(mode='w') as tempf:
+ ase.io.cube.write_cube(tempf, atoms_xn, data_xn)
+ c_2 = self.viewer.add_component(tempf.name, ext='cube')
+ c_2.clear()
+
+ def set_cube_isosurf(self, isovals, colors):
+ """Set cube isosurface."""
+ # pylint: disable=no-member
+ if hasattr(self.viewer, 'component_1'):
+ c_2 = self.viewer.component_1
+ c_2.clear()
+ for isov, col in zip(isovals, colors):
+ c_2.add_surface(color=col, isolevelType="value", isolevel=isov)
+
+
+class CubeArrayData2dViewerWidget(ipw.VBox):
+ """Widget to View 3-dimensional AiiDA ArrayData object projected on 2D plane."""
+ arraydata = Instance(ArrayData, allow_none=True)
+
+ def __init__(self, **kwargs):
+ self.structure = None
+ self._current_structure = None
+ self.selected_data = None
+ self._current_data = None
+ self.units = None
+ layout = ipw.Layout(padding="5px", margin="0px")
+ self.plot = ipw.Output(layout=layout)
+ self.height_slider = ipw.SelectionSlider(description="Height",
+ options={"---": 0},
+ continuous_update=False,
+ layout=layout)
+ self.height_slider.observe(self.update_plot, names='value')
+ self.opacity_slider = ipw.FloatSlider(description="Opacity",
+ value=0.5,
+ max=1.0,
+ continuous_update=False,
+ layout=layout)
+ self.opacity_slider.observe(self.update_plot, names='value')
+ self.colormap_slider = ipw.FloatLogSlider(value=0.2,
+ min=-5,
+ max=1,
+ step=0.1,
+ description='Color max.',
+ continuous_update=False,
+ layout=layout)
+ self.colormap_slider.observe(self.update_plot, names='value')
+
+ self.axis = ipw.ToggleButtons(options=[('X', [1, 2, 0]), ('Y', [2, 0, 1]), ('Z', [0, 1, 2])],
+ value=[0, 1, 2],
+ description='Axis:',
+ orientation='horizontal',
+ disabled=True,
+ style={'button_width': 'initial'})
+ self.axis.observe(self.update_axis, names='value')
+
+ super().__init__([self.plot, self.axis, self.height_slider, self.colormap_slider, self.opacity_slider],
+ **kwargs)
+
+ @observe('arraydata')
+ def on_observe_arraydata(self, _=None):
+ """Update object attributes when arraydata trait is modified."""
+ self.selected_data = self.arraydata.get_array('data')
+ cell = self.arraydata.get_array('voxel') / ANG_2_BOHR
+ for i in range(3):
+ cell[i, :] *= self.selected_data.shape[i]
+ self.structure = ase.Atoms(
+ numbers=self.arraydata.get_array('atomic_numbers'),
+ positions=self.arraydata.get_array('coordinates') / ANG_2_BOHR,
+ cell=cell,
+ pbc=True,
+ )
+ self.units = self.arraydata.get_array('data_units')
+ self.update_axis()
+ self.update_plot()
+
+ def update_axis(self, _=None):
+ """Make plot perpencicular to the selected axis."""
+ # Adapting (transposing) the data
+ self._current_data = self.selected_data.transpose(self.axis.value)
+
+ # Adapting (transposing) coordinates and positions.
+ rot = {
+ 'X': ('x', 'z'),
+ 'Y': ('y', 'z'),
+ 'Z': ('z', 'z'),
+ }
+ self._current_structure = self.structure.copy()
+ self._current_structure.rotate(*rot[self.axis.label], rotate_cell=True)
+
+ n_z = self._current_data.shape[2]
+ d_z = self._current_structure.cell[2][2] / n_z
+ options = OrderedDict()
+
+ for i in range(0, n_z, 3):
+ options[u"{:.3f} Å".format(d_z * i)] = i
+ self.height_slider.options = options
+ nopt = int(len(options) / 2)
+ self.height_slider.value = list(options.values())[nopt]
+
+ def update_plot(self, _=None):
+ """Update the 2D plot with the new data."""
+ with self.plot:
+ clear_output()
+ fig, axplt = plt.subplots()
+ fig.dpi = 150.0
+ vmax = np.max(np.abs(self._current_data)) * self.colormap_slider.value
+
+ flipped_data = np.flip(self._current_data[:, :, self.height_slider.value].transpose(), axis=0)
+
+ x_2 = self._current_structure.cell[0][0] * 2.0
+ y_2 = self._current_structure.cell[1][1]
+
+ # Set labels and limits.
+ axplt.set_xlabel(u'Å')
+ axplt.set_ylabel(u'Å')
+ axplt.set_xlim(0, x_2)
+ axplt.set_ylim(0, y_2)
+
+ fig.colorbar(axplt.imshow(np.tile(flipped_data, (1, 2)),
+ extent=[0, x_2, 0, y_2],
+ cmap='seismic',
+ vmin=-vmax,
+ vmax=vmax),
+ label=self.units,
+ ticks=[-vmax, vmax],
+ format='%.e',
+ orientation='horizontal',
+ shrink=0.3)
+
+ plot_struct_2d(axplt, self._current_structure, self.opacity_slider.value)
+ plt.show()
+
+
+class NanoribbonShowWidget(ipw.VBox):
+ """Show the results of a nanoribbon work chain."""
+
+ def __init__(self, workcalc, **kwargs):
+ self._workcalc = workcalc
+
+ self.info = ipw.HTML(
+ NANORIBBON_INFO.format(
+ pk=workcalc.id,
+ energy=workcalc.extras['total_energy'],
+ gap=workcalc.extras['gap'],
+ totmagn=workcalc.extras['absolute_magnetization_per_angstr'],
+ absmagn=workcalc.extras['total_magnetization_per_angstr'],
+ ))
+
+ self.orbitals_calcs = get_calcs_by_label(workcalc, "export_orbitals")
+ bands_calc = get_calc_by_label(workcalc, "bands")
+ self.selected_cube_files = []
+ self.bands_viewer = BandsViewerWidget(
+ bands=bands_calc.outputs.output_band,
+ nelectrons=int(bands_calc.outputs.output_parameters['number_of_electrons']),
+ vacuum_level=self._workcalc.get_extra('vacuum_level'),
+ structure=bands_calc.inputs.structure,
+ homo=self._workcalc.get_extra('homo'),
+ lumo=self._workcalc.get_extra('lumo'),
+ gap=self._workcalc.get_extra('gap'),
+ )
+ self.bands_viewer.observe(self.on_band_change, names='selected_band')
+ self.bands_viewer.observe(self.on_kpoint_change, names='selected_kpoint')
+
+ self.orbital_viewer_2d = CubeArrayData2dViewerWidget()
+ self.orbital_viewer_3d = CubeArrayData3dViewerWidget()
+ self.spinden_viewer_2d = CubeArrayData2dViewerWidget()
+ self.spinden_viewer_3d = CubeArrayData3dViewerWidget()
+ self.info_out = ipw.HTML()
+
+ if self.spindensity_calc:
+ try:
+ self.spinden_viewer_2d.arraydata = self.spindensity_calc.outputs.output_data
+ self.spinden_viewer_3d.arraydata = self.spindensity_calc.outputs.output_data
+ except exceptions.NotExistent:
+ with gzip.open(self.spindensity_calc.outputs.retrieved.open("_spin.cube.gz").name) as fpointer:
+ arrayd = from_cube_to_arraydata(fpointer.read())
+ self.spinden_viewer_2d.arraydata = arrayd
+ self.spinden_viewer_3d.arraydata = arrayd
+
+ self.on_band_change()
+
+ super().__init__([
+ self.info,
+ ipw.HBox([
+ self.bands_viewer,
+ ipw.VBox([self.info_out, ipw.HBox([self.orbital_viewer_2d, self.orbital_viewer_3d])],
+ layout=ipw.Layout(margin="200px 0px 0px 0px")),
+ ]),
+ ipw.HBox([self.spinden_viewer_2d, self.spinden_viewer_3d]),
+ ], **kwargs)
+
+ def on_band_change(self, _=None):
+ """Replot the orbitals in case of the the band change."""
+
+ # Orbitals_calcs might use fewer nkpoints than bands_calc.
+ prev_calc = self.orbitals_calcs[0].inputs.parent_folder.creator
+ nkpoints_lowres = prev_calc.res.number_of_k_points
+
+ lower = nkpoints_lowres * self.bands_viewer.spin_selector.value
+ upper = lower + nkpoints_lowres
+ self.selected_cube_files = []
+
+ list_of_calcs = []
+ for orbitals_calc in self.orbitals_calcs:
+ if any(['output_data_multiple' in x for x in orbitals_calc.outputs]):
+ list_of_calcs += [(x, orbitals_calc) for x in orbitals_calc.outputs]
+ else:
+ list_of_calcs += [(x.name, orbitals_calc) for x in orbitals_calc.outputs.retrieved.list_objects()]
+ for fname, orbitals_calc in sorted(list_of_calcs, key=lambda x: x[0]):
+ mtch = re.match(r".*_K(\d\d\d)_B(\d\d\d).*", fname)
+ if mtch:
+ kpnt, bnd = int(mtch.group(1)), int(mtch.group(2))
+ if bnd != self.bands_viewer.selected_band + 1:
+ continue
+ if lower < kpnt <= upper:
+ if fname.startswith('output_data_multiple'):
+ self.selected_cube_files.append(orbitals_calc.outputs[fname])
+ else:
+ self.selected_cube_files.append(orbitals_calc.outputs.retrieved.open(fname).name)
+
+ self.info_out.value = "Found {} cube files".format(len(self.selected_cube_files))
+ self.on_kpoint_change(None)
+
+ def on_kpoint_change(self, _=None):
+ """Replot the orbitals in case of the kpoint change."""
+ if self.bands_viewer.selected_kpoint > len(self.selected_cube_files):
+ print("Found no cube files")
+ self.orbital_viewer_3d.arraydata = None
+ self.orbital_viewer_2d.arraydata = None
+ else:
+ if isinstance(self.selected_cube_files[self.bands_viewer.selected_kpoint - 1], ArrayData):
+ arraydata = self.selected_cube_files[self.bands_viewer.selected_kpoint - 1]
+ else:
+ absfn = self.selected_cube_files[self.bands_viewer.selected_kpoint - 1]
+ with gzip.open(absfn) as fpointer:
+ arraydata = from_cube_to_arraydata(fpointer.read())
+ self.orbital_viewer_2d.arraydata = arraydata
+ self.orbital_viewer_3d.arraydata = arraydata
+
@property
def spindensity_calc(self):
+ """Return spindensity plot calculation if present, otherwise return None."""
try:
return get_calc_by_label(self._workcalc, "export_spinden")
- except:
+ except AssertionError:
return None
-
- def spindensity_2d(self):
- if self.spindensity_calc:
- #spinden_cube = read_cube(self.spindensity_calc.outputs.retrieved.open("_spin.cube.gz").name)
- data,atoms = ase.io.cube.read_cube_data(self.spindensity_calc.outputs.retrieved.open("_spin.cube.gz").name)
- #spinden_cube['data'] *= 2000 # normalize scale
- datascaled = data*2000
- def on_spinden_plot_change(c):
- with spinden_out:
- clear_output()
- fig, ax = plt.subplots()
- fig.dpi = 150.0
- cax = plot_cuben(ax, datascaled,atoms, 1, 'seismic')
- fig.colorbar(cax, label='arbitrary unit')
- #self.plot_overlay_struct(ax, spinden_alpha_slider.value)
- self.plot_overlay_structn(ax,atoms, spinden_alpha_slider.value)
- plt.show()
-
- spinden_alpha_slider = ipw.FloatSlider(description="opacity", value=0.5, max=1.0, continuous_update=False)
- spinden_alpha_slider.observe(on_spinden_plot_change, names='value')
- spinden_out = ipw.Output()
- on_spinden_plot_change(None)
- return ipw.VBox([spinden_out, spinden_alpha_slider])
- else:
- print("Could not find spin density")
-
- def spindensity_3d(self):
- if self.spindensity_calc:
- file_path= None
- retrieved_files = self.spindensity_calc.outputs.retrieved.list_object_names()
- if "_spin_full.cube.gz" in retrieved_files:
- file_path = self.spindensity_calc.outputs.retrieved.open("_spin_full.cube.gz").name
- elif "_spin.cube.gz" in retrieved_files:
- file_path = self.spindensity_calc.outputs.retrieved.open("_spin.cube.gz").name
- else:
- print("Spin density cube could not be visualized, file was not retrieved.")
-
- if file_path:
- ngl_view = nglview.NGLWidget()
- setup_cube_plot(file_path, ngl_view)
- isosurf_slider = ipw.FloatSlider(continuous_update=False,
- value=1e-3,
- min=1e-4,
- max=1e-2,
- step=1e-4,
- description='isovalue',
- readout_format='.1e'
- )
- isosurf_slider.observe(lambda c: set_cube_isosurf([c['new'], -c['new']], ['red', 'blue'], ngl_view), names='value')
-
- set_cube_isosurf([isosurf_slider.value, -isosurf_slider.value], ['red', 'blue'], ngl_view)
-
- return ipw.VBox([ngl_view, isosurf_slider])
-
- def orbital_3d(self):
- if self.selected_cube_file is not None:
-
- # delete all old components
- while hasattr(self.orbital_ngl, "component_0"):
- self.orbital_ngl.component_0.clear_representations()
- cid = self.orbital_ngl.component_0.id
- self.orbital_ngl.remove_component(cid)
-
- setup_cube_plot(self.selected_cube_file, self.orbital_ngl)
- set_cube_isosurf([self.orb_isosurf_slider.value], ['red'], self.orbital_ngl)
-
-
diff --git a/nanoribbon/viewers/smiles2GNR.py b/nanoribbon/viewers/smiles2GNR.py
deleted file mode 100644
index ffe9aa4..0000000
--- a/nanoribbon/viewers/smiles2GNR.py
+++ /dev/null
@@ -1,293 +0,0 @@
-#import numpy as np
-#from scipy.stats import mode
-#from numpy.linalg import norm
-#from pysmiles import read_smiles,write_smiles
-#from rdkit.Chem.rdmolfiles import MolFromSmiles,MolToMolFile
-#import networkx as nx
-#import math
-#from ase import Atoms
-#from ase.visualize import view
-from IPython.display import display, clear_output
-import ipywidgets as ipw
-import nglview
-#from ase.data import covalent_radii
-#from ase.neighborlist import NeighborList
-#import ase.neighborlist
-import numpy as np
-from numpy.linalg import norm
-
-from traitlets import Instance
-
-from ase import Atoms
-from ase.data import covalent_radii
-from ase.neighborlist import NeighborList
-import ase.neighborlist
-
-
-from rdkit.Chem import AllChem
-from rdkit.Chem.rdmolfiles import MolFromSmiles,MolToMolFile
-
-
-
-class Smiles2GNRWidget(ipw.VBox):
- """Conver SMILES into 3D structure."""
- structure = Instance(Atoms, allow_none=True)
- SPINNER = """"""
-
- def __init__(self):
- try:
- import openbabel # pylint: disable=unused-import
- except ImportError:
- super().__init__(
- [ipw.HTML("The SmilesWidget requires the OpenBabel library, "
- "but the library was not found.")])
- return
-
- self.selection = set()
- self.cell_ready = False
- self.smiles = ipw.Text()
- self.create_structure_btn = ipw.Button(description="Convert SMILES", button_style='info')
- def print_hello(change):
- print(change)
- self.create_structure_btn.on_click(self._on_button_pressed)
- #self.create_structure_btn.on_click(print_hello)
- self.create_cell_btn = ipw.Button(description="create GNR", button_style='info',disabled=True)
- self.create_cell_btn.on_click(self._on_button2_pressed)
- self.viewer = nglview.NGLWidget()
- self.viewer.stage.set_parameters(mouse_preset='pymol')
- self.viewer.observe(self._on_picked, names='picked')
- self.select_two = ipw.HTML("")
- self.output = ipw.HTML("")
- self.picked_out = ipw.Output()
- self.button2_out = ipw.Output()
- super().__init__([self.smiles, ipw.Label(value="e.g. C1(C2=CC=C(C3=CC=CC=C3)C=C2)=CC=CC=C1"),
- self.create_structure_btn,
- self.select_two,
- self.viewer,
- self.picked_out, self.output,self.create_cell_btn, self.button2_out])
-
-########
- @staticmethod
- def guess_scaling_factor(atoms):
- import numpy as np
- from numpy.linalg import norm
- from scipy.stats import mode
- from ase import Atoms
- # set bounding box as cell
- cx = 1.5 * (np.amax(atoms.positions[:,0]) - np.amin(atoms.positions[:,0]))
- cy = 1.5 * (np.amax(atoms.positions[:,1]) - np.amin(atoms.positions[:,1]))
- cz = 15.0
- atoms.cell = (cx, cy, cz)
- atoms.pbc = (True,True,True)
-
- # calculate all atom-atom distances
- c_atoms = [a for a in atoms if a.symbol[0]=="C"]
- n = len(c_atoms)
- dists = np.zeros([n,n])
- for i, a in enumerate(c_atoms):
- for j, b in enumerate(c_atoms):
- dists[i,j] = norm(a.position - b.position)
-
- # find bond distances to closest neighbor
- dists += np.diag([np.inf]*n) # don't consider diagonal
- bonds = np.amin(dists, axis=1)
-
- # average bond distance
- avg_bond = float(mode(bonds)[0])
-
- # scale box to match equilibrium carbon-carbon bond distance
- cc_eq = 1.4313333333
- s = cc_eq / avg_bond
- return s
-
- @staticmethod
- def scale(atoms, s):
- from ase import Atoms
- cx, cy, cz = atoms.cell
- atoms.set_cell((s*cx, s*cy, cz), scale_atoms=True)
- atoms.center()
- return atoms
-
- @staticmethod
- def smiles2D(smiles):
-
-
- mol = MolFromSmiles(smiles)
-
- AllChem.Compute2DCoords(mol)
- # get the 2D coordinates
-
- for c in mol.GetConformers():
- coords=c.GetPositions()
-
- # get the atom labels
- ll=[]
- for i in mol.GetAtoms():
- #ll.append(i.GetSymbol())
- ll.append(i.GetAtomicNum())
- ll=np.asarray(ll)
-
- # create an ASE frame
- c=Atoms('{:d}N'.format(len(coords)))
- c.set_positions(coords)
- c.set_atomic_numbers(ll)
- return c
-
- @staticmethod
- def construct_cell(atoms, id1, id2):
-
- p1 = [atoms[id1].x, atoms[id1].y]
- p0 = [atoms[id2].x, atoms[id2].y]
- p2 = [atoms[id2].x, atoms[id1].y]
-
- v0 = np.array(p0) - np.array(p1)
- v1 = np.array(p2) - np.array(p1)
-
- angle = np.math.atan2(np.linalg.det([v0,v1]),np.dot(v0,v1))
-
- #angle=np.degrees(angle)
-
- cx = norm(v0)
-
- #print np.degrees(angle),v0,v1,p0,p1
- if np.abs(angle) > 0.01:
- # s.euler_rotate(phi=angle,theta=0,psi=0,center(x[id1],y[id1],z[id1]))
- atoms.rotate_euler(center=atoms[id1].position, phi=-angle,theta=0.0,psi=0.0)
-
- yrange = np.amax(atoms.positions[:,1])-np.amin(atoms.positions[:,1])
- zrange = np.amax(atoms.positions[:,2])-np.amin(atoms.positions[:,2])
- cy = 15.0 + yrange
- cz = 15.0 + zrange
-
- atoms.cell = (cx,cy,cz)
- atoms.pbc = (True,True,True)
- atoms.center()
- atoms.wrap(eps=0.001)
-
- #### REMOVE REDUNDANT ATOMS
- tobedel = []
-
- cov_radii = [covalent_radii[a.number] for a in atoms]
- nl = NeighborList(cov_radii, bothways = False, self_interaction = False)
- nl.update(atoms)
-
- for a in atoms:
- indices, offsets = nl.get_neighbors(a.index)
- for i, offset in zip(indices, offsets):
- dist = norm(a.position -(atoms.positions[i] + np.dot(offset, atoms.get_cell())))
- if dist < 0.4 :
- tobedel.append(atoms[i].index)
-
- del atoms[tobedel]
-
- #### ENDFIND UNIT CELL AND APPLIES IT
-
- #### ADD Hydrogens
- cov_radii = [covalent_radii[a.number] for a in atoms]
- nl = NeighborList(cov_radii, bothways = True, self_interaction = False)
- nl.update(atoms)
-
- need_a_H = []
- for a in atoms:
- nlist=nl.get_neighbors(a.index)[0]
- if len(nlist)<3:
- if a.symbol=='C':
- need_a_H.append(a.index)
-
- print("Added missing Hydrogen atoms: ", need_a_H)
-
- dCH=1.1
- for a in need_a_H:
- vec = np.zeros(3)
- indices, offsets = nl.get_neighbors(atoms[a].index)
- for i, offset in zip(indices, offsets):
- vec += -atoms[a].position +(atoms.positions[i] + np.dot(offset, atoms.get_cell()))
- vec = -vec/norm(vec)*dCH
- vec += atoms[a].position
- htoadd = ase.Atom('H',vec)
- atoms.append(htoadd)
-
- return atoms
-
- def _on_picked(self,ca):
-
- self.cell_ready = False
-
-
- if 'atom1' not in self.viewer.picked.keys():
- return # did not click on atom
- self.create_cell_btn.disabled = True
-
- with self.picked_out:
- clear_output()
-
- #viewer.clear_representations()
- self.viewer.component_0.remove_ball_and_stick()
- self.viewer.component_0.remove_ball_and_stick()
- self.viewer.add_ball_and_stick()
- #viewer.add_unitcell()
-
- idx = self.viewer.picked['atom1']['index']
-
- # toggle
- if idx in self.selection:
- self.selection.remove(idx)
- else:
- self.selection.add(idx)
-
- if len(self.selection) == 2:
- self.create_cell_btn.disabled = False
-
- #if(selection):
- sel_str = ",".join([str(i) for i in sorted(self.selection)])
- print("Selected atoms: "+ sel_str )
- self.viewer.add_representation('ball+stick', selection="@"+sel_str, color='red', aspectRatio=3.0)
- #else:
- # print ("nothing selected")
- self.viewer.picked = {} # reset, otherwise immidiately selecting same atom again won't create change event
-
-
-
- def _on_button_pressed(self, change): # pylint: disable=unused-argument
- """Convert SMILES to ase structure when button is pressed."""
- self.output.value = ""
- self.select_two.value='