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 = ' 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='

Select two equivalent atoms that define the basis vector

' - self.create_cell_btn.disabled=True - if not self.smiles.value: - return - - smiles=self.smiles.value.replace(" ", "") - c=self.smiles2D(smiles) - # set the cell - - scaling_fac=self.guess_scaling_factor(c) - scaled_structure=self.scale(c,scaling_fac) - self.original_structure=c.copy() - if hasattr(self.viewer, "component_0"): - self.viewer.component_0.remove_ball_and_stick() - #viewer.component_0.remove_unitcell() - cid = self.viewer.component_0.id - self.viewer.remove_component(cid) - - # empty selection - self.selection = set() - self.cell_ready = False - - # add new component - self.viewer.add_component(nglview.ASEStructure(c)) # adds ball+stick - #viewer.add_unitcell() - self.viewer.center() - - def _on_button2_pressed(self, change): - with self.button2_out: - clear_output() - self.cell_ready = False - - if len(self.selection) != 2: - print("You must select exactly two atoms") - return - - - id1 = sorted(self.selection)[0] - id2 = sorted(self.selection)[1] - self.structure = self.construct_cell(self.original_structure, id1, id2) \ No newline at end of file diff --git a/nanoribbon/viewers/smiles2gnr.py b/nanoribbon/viewers/smiles2gnr.py new file mode 100644 index 0000000..50ba21d --- /dev/null +++ b/nanoribbon/viewers/smiles2gnr.py @@ -0,0 +1,236 @@ +# pylint: disable=no-member +"""Widget to convert SMILES to nanoribbons.""" + +import numpy as np +from scipy.stats import mode + +from IPython.display import clear_output +import ipywidgets as ipw +import nglview + +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 + + +class Smiles2GnrWidget(ipw.VBox): + """Conver SMILES into 3D structure.""" + structure = Instance(Atoms, allow_none=True) + SPINNER = """""" + + def __init__(self, title="Smiles to GNR"): + 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.title = title + self.original_structure = None + self.selection = set() + self.smiles = ipw.Text() + + create_structure_btn = ipw.Button(description="Convert SMILES", button_style='info') + create_structure_btn.on_click(self._on_button_pressed) + + self.create_cell_btn = ipw.Button(description="create GNR", button_style='info', disabled=True) + self.create_cell_btn.on_click(self._on_cell_button_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.picked_out = ipw.Output() + self.cell_button_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"), create_structure_btn, self.select_two, + self.viewer, self.picked_out, self.create_cell_btn, self.cell_button_out + ]) + + @staticmethod + def guess_scaling_factor(atoms): + """Scaling factor to correct the bond length.""" + + # Set bounding box as cell. + c_x = 1.5 * (np.amax(atoms.positions[:, 0]) - np.amin(atoms.positions[:, 0])) + c_y = 1.5 * (np.amax(atoms.positions[:, 1]) - np.amin(atoms.positions[:, 1])) + c_z = 15.0 + atoms.cell = (c_x, c_y, c_z) + atoms.pbc = (True, True, True) + + # Calculate all atom-atom distances. + c_atoms = [a for a in atoms if a.symbol[0] == "C"] + n_atoms = len(c_atoms) + dists = np.zeros([n_atoms, n_atoms]) + for i, atom_a in enumerate(c_atoms): + for j, atom_b in enumerate(c_atoms): + dists[i, j] = np.linalg.norm(atom_a.position - atom_b.position) + + # Find bond distances to closest neighbor. + dists += np.diag([np.inf] * n_atoms) # 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 + return cc_eq / avg_bond + + @staticmethod + def scale(atoms, factor): + """Scale atomic positions by the `factor`.""" + c_x, c_y, c_z = atoms.cell + atoms.set_cell((factor * c_x, factor * c_y, c_z), scale_atoms=True) + atoms.center() + return atoms + + @staticmethod + def smiles2d(smiles): + """Create planar molecule from smiles.""" + mol = MolFromSmiles(smiles) + + # Get the 2D coordinates. + AllChem.Compute2DCoords(mol) + + for struct in mol.GetConformers(): + coords = struct.GetPositions() + + # Create an ASE frame. + struct = Atoms('{:d}N'.format(len(coords)), positions=coords) + struct.set_atomic_numbers(np.asarray([i.GetAtomicNum() for i in mol.GetAtoms()])) + return struct + + @staticmethod + def construct_cell(atoms, id1, id2): + """Construct periodic cell based on two selected equivalent atoms.""" + + pos = [[atoms[id2].x, atoms[id2].y], [atoms[id1].x, atoms[id1].y], [atoms[id2].x, atoms[id1].y]] + + vec = [np.array(pos[0]) - np.array(pos[1]), np.array(pos[2]) - np.array(pos[1])] + c_x = np.linalg.norm(vec[0]) + + angle = np.math.atan2(np.linalg.det([vec[0], vec[1]]), np.dot(vec[0], vec[1])) + if np.abs(angle) > 0.01: + atoms.rotate_euler(center=atoms[id1].position, phi=-angle, theta=0.0, psi=0.0) + + c_y = 15.0 + np.amax(atoms.positions[:, 1]) - np.amin(atoms.positions[:, 1]) + c_z = 15.0 + np.amax(atoms.positions[:, 2]) - np.amin(atoms.positions[:, 2]) + + atoms.cell = (c_x, c_y, c_z) + atoms.pbc = (True, True, True) + atoms.center() + atoms.wrap(eps=0.001) + + # Remove redundant atoms. + tobedel = [] + + n_l = NeighborList([covalent_radii[a.number] for a in atoms], bothways=False, self_interaction=False) + n_l.update(atoms) + + for atm in atoms: + indices, offsets = n_l.get_neighbors(atm.index) + for i, offset in zip(indices, offsets): + dist = np.linalg.norm(atm.position - (atoms.positions[i] + np.dot(offset, atoms.get_cell()))) + if dist < 0.4: + tobedel.append(atoms[i].index) + + del atoms[tobedel] + + # Find unit cell and apply it. + + ## Add Hydrogens. + n_l = NeighborList([covalent_radii[a.number] for a in atoms], bothways=True, self_interaction=False) + n_l.update(atoms) + + need_hydrogen = [] + for atm in atoms: + if len(n_l.get_neighbors(atm.index)[0]) < 3: + if atm.symbol == 'C': + need_hydrogen.append(atm.index) + + print("Added missing Hydrogen atoms: ", need_hydrogen) + + for atm in need_hydrogen: + vec = np.zeros(3) + indices, offsets = n_l.get_neighbors(atoms[atm].index) + for i, offset in zip(indices, offsets): + vec += -atoms[atm].position + (atoms.positions[i] + np.dot(offset, atoms.get_cell())) + vec = -vec / np.linalg.norm(vec) * 1.1 + atoms[atm].position + atoms.append(ase.Atom('H', vec)) + + return atoms + + def _on_picked(self, _=None): + """When an attom is picked.""" + + 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() + + self.viewer.component_0.remove_ball_and_stick() + self.viewer.component_0.remove_ball_and_stick() + self.viewer.add_ball_and_stick() + + 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) + self.viewer.picked = {} # reset, otherwise immidiately selecting same atom again won't create change event + + def _on_button_pressed(self, _=None): + """Convert SMILES to ase structure when button is pressed.""" + self.select_two.value = '

Select two equivalent atoms that define the basis vector

' + self.create_cell_btn.disabled = True + if not self.smiles.value: + return + + smiles = self.smiles.value.replace(" ", "") + struct = self.smiles2d(smiles) + self.original_structure = struct.copy() + if hasattr(self.viewer, "component_0"): + self.viewer.component_0.remove_ball_and_stick() + cid = self.viewer.component_0.id + self.viewer.remove_component(cid) + + # Empty selection. + self.selection = set() + + # Add new component. + self.viewer.add_component(nglview.ASEStructure(struct)) # adds ball+stick + self.viewer.center() + self.viewer.handle_resize() + + def _on_cell_button_pressed(self, _=None): + """Generate GNR button pressed.""" + with self.cell_button_out: + clear_output() + if len(self.selection) != 2: + print("You must select exactly two atoms") + return + + id1 = sorted(self.selection)[0] + id2 = sorted(self.selection)[1] + self.structure = self.construct_cell(self.original_structure, id1, id2) diff --git a/nanoribbon/viewers/utils.py b/nanoribbon/viewers/utils.py new file mode 100644 index 0000000..965770a --- /dev/null +++ b/nanoribbon/viewers/utils.py @@ -0,0 +1,111 @@ +"""Utility functions for the nanoribbon workchain viewers.""" + +import numpy as np +import matplotlib.pyplot as plt + +from ase.data import covalent_radii, atomic_numbers +from ase.neighborlist import NeighborList +from ase.data.colors import cpk_colors + +# AiiDA imports +from aiida.plugins import DataFactory +from aiida.orm import CalcJobNode, QueryBuilder, WorkChainNode + +# AiiDA data types. +ArrayData = DataFactory("array") # pylint: disable=invalid-name + + +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): + """Get step calculation of a workchain by its name.""" + qbld = QueryBuilder() + qbld.append(WorkChainNode, filters={'uuid': workcalc.uuid}) + qbld.append(CalcJobNode, with_incoming=WorkChainNode, filters={'label': label}) + calcs = [c[0] for c in qbld.all()] + for calc in calcs: + assert calc.is_finished_ok + return calcs + + +def from_cube_to_arraydata(cube_content): + """Convert cube file to the AiiDA ArrayData object.""" + lines = cube_content.splitlines() + natoms = int(lines[2].split()[0]) # The number of atoms listed in the file + header = lines[:6 + natoms] # Header of the file: comments, the voxel, and the number of atoms and datapoints + + # Parse the declared dimensions of the volumetric data + x_line = header[3].split() + xdim = int(x_line[0]) + y_line = header[4].split() + ydim = int(y_line[0]) + z_line = header[5].split() + zdim = int(z_line[0]) + + # Get the vectors describing the basis voxel + voxel_array = np.array( + [[x_line[1], x_line[2], x_line[3]], [y_line[1], y_line[2], y_line[3]], [z_line[1], z_line[2], z_line[3]]], + dtype=np.float64) + atm_numbers = np.empty(natoms, int) + coordinates = np.empty((natoms, 3)) + for i in range(natoms): + line = header[6 + i].split() + atm_numbers[i] = int(line[0]) + coordinates[i] = [float(s) for s in line[2:]] + + # Get the volumetric data + data_array = np.empty(xdim * ydim * zdim, dtype=float) + cursor = 0 + for line in lines[6 + natoms:]: # The actual data: atoms and volumetric data + lsplitted = line.split() + data_array[cursor:cursor + len(lsplitted)] = lsplitted + cursor += len(lsplitted) + + arraydata = ArrayData() + arraydata.set_array('voxel', voxel_array) + arraydata.set_array('data', data_array.reshape((xdim, ydim, zdim))) + arraydata.set_array('data_units', np.array('e/bohr^3')) + arraydata.set_array('coordinates_units', np.array('bohr')) + arraydata.set_array('coordinates', coordinates) + arraydata.set_array('atomic_numbers', atm_numbers) + + return arraydata + + +def plot_struct_2d(ax_plt, atoms, alpha): + """Plot structure on 2d matplotlib plot.""" + if alpha == 0: + return + + # Plot overlayed structure. + strct = atoms.repeat((2, 1, 1)) + cov_radii = [covalent_radii[a.number] for a in strct] + nlist = NeighborList(cov_radii, bothways=True, self_interaction=False) + nlist.update(strct) + + for atm in strct: + # Circles. + pos = atm.position + nmbrs = atomic_numbers[atm.symbol] + ax_plt.add_artist( + plt.Circle((pos[0], pos[1]), + covalent_radii[nmbrs] * 0.5, + color=cpk_colors[nmbrs], + fill=True, + clip_on=True, + alpha=alpha)) + + # Bonds. + for theneig in nlist.get_neighbors(atm.index)[0]: + pos = (strct[theneig].position + atm.position) / 2 + pos0 = atm.position + if (pos[0] - pos0[0])**2 + (pos[1] - pos0[1])**2 < 2: + ax_plt.plot([pos0[0], pos[0]], [pos0[1], pos[1]], + color=cpk_colors[nmbrs], + linewidth=2, + linestyle='-', + alpha=alpha) diff --git a/nanoribbon/workchains/aux_script_strings.py b/nanoribbon/workchains/aux_script_strings.py deleted file mode 100644 index e5487eb..0000000 --- a/nanoribbon/workchains/aux_script_strings.py +++ /dev/null @@ -1,200 +0,0 @@ - -cube_cutter = r""" -cat > cube_cutter.py << EOF - -from glob import glob -import numpy as np -import gzip - -for fn in glob("*.cube"): - # parse - lines = open(fn).readlines() - header = np.fromstring("".join(lines[2:6]), sep=' ').reshape(4,4) - natoms, nx, ny, nz = header[:,0].astype(int) - cube = np.fromstring("".join(lines[natoms+6:]), sep=' ').reshape(nx, ny, nz) - - # plan - dz = header[3,3] - angstrom = int(1.88972 / dz) - z0 = nz/2 + 1*angstrom # start one angstrom above surface - z1 = z0 + 3*angstrom # take three layers at one angstrom distance - zcuts = range(z0, z1+1, angstrom) - - # output - ## change offset header - lines[2] = "{:5d} 0.000000 0.000000 {:12.6f}\n".format(natoms, z0*dz) - ## change shape header - lines[5] = "{:6d} 0.000000 0.000000 {:12.6f}\n".format(len(zcuts), angstrom*dz) - with gzip.open(fn+".gz", "w") as f: - f.write("".join(lines[:natoms+6])) # write header - np.savetxt(f, cube[:,:,zcuts].reshape(-1, len(zcuts)), fmt="%.5e") -EOF - -python ./cube_cutter.py -""" - -cube_clipper_cropper = r""" -cat > cube_clipper_cropper.py << EOF - - -import numpy as np -import itertools - -import io - -import gzip -from glob import glob - -ang_2_bohr = 1.889725989 - - #CUBE_PATH = "./_spin.cube" - -def read_cube_file(file_lines): - - fit = iter(file_lines) - - title = next(fit) - comment = next(fit) - - line = next(fit).split() - natoms = int(line[0]) - - origin = np.array(line[1:], dtype=float) - - shape = np.empty(3,dtype=int) - cell = np.empty((3, 3)) - for i in range(3): - n, x, y, z = [float(s) for s in next(fit).split()] - shape[i] = int(n) - cell[i] = n * np.array([x, y, z]) - - numbers = np.empty(natoms, int) - positions = np.empty((natoms, 3)) - for i in range(natoms): - line = next(fit).split() - numbers[i] = int(line[0]) - positions[i] = [float(s) for s in line[2:]] - - positions /= ang_2_bohr # convert from bohr to ang - - data = np.empty(shape[0]*shape[1]*shape[2], dtype=float) - cursor = 0 - for i, line in enumerate(fit): - ls = line.split() - data[cursor:cursor+len(ls)] = ls - cursor += len(ls) - - data = data.reshape(shape) - - cell /= ang_2_bohr # convert from bohr to ang - - return numbers, positions, cell, origin, data - -def write_cube_file_gzip(filename, numbers, positions, cell, data, origin = np.array([0.0, 0.0, 0.0])): - - positions *= ang_2_bohr - origin *= ang_2_bohr - - natoms = positions.shape[0] - - f = gzip.open(filename, 'w') - - f.write(filename+'\n') - - f.write('cube\n') - - dv_br = cell*ang_2_bohr/data.shape - - f.write("{:5d} {:12.6f} {:12.6f} {:12.6f}\n".format(natoms, origin[0], origin[1], origin[2])) - - for i in range(3): - f.write("{:5d} {:12.6f} {:12.6f} {:12.6f}\n".format(data.shape[i], dv_br[i][0], dv_br[i][1], dv_br[i][2])) - - for i in range(natoms): - at_x, at_y, at_z = positions[i] - f.write("{:5d} {:12.6f} {:12.6f} {:12.6f} {:12.6f}\n".format(numbers[i], 0.0, at_x, at_y, at_z)) - - # 6 columns !!! - - fmt=' {:11.4e}' - for ix in range(data.shape[0]): - for iy in range(data.shape[1]): - for line in range(data.shape[2] // 6 ): - f.write((fmt*6 + "\n").format(*data[ix, iy, line*6 : (line+1)*6])) - left = data.shape[2] % 6 - if left != 0: - f.write((fmt*left + "\n").format(*data[ix, iy, -left:])) - - f.close() - - -def clip_data(data, absmin=None, absmax=None): - if absmin: - data[np.abs(data) < absmin] = 0 - if absmax: - data[data > absmax] = absmax - data[data < -absmax] = -absmax - -def crop_cube(data, pos, cell, origin, x_crop=None, y_crop=None, z_crop=None): - - dv = np.diag(cell)/data.shape - - # corners of initial box - i_p0 = origin - i_p1 = origin + np.diag(cell) - - # corners of cropped box - c_p0 = np.copy(i_p0) - c_p1 = np.copy(i_p1) - - for i, i_crop in enumerate([x_crop, y_crop, z_crop]): - pmax, pmin = np.max(pos[:, i]), np.min(pos[:, i]) - - if i_crop: - c_p0[i] = pmin - i_crop/2 - c_p1[i] = pmax + i_crop/2 - - # make grids match - shift_0 = (c_p0[i] - i_p0[i]) % dv[i] - c_p0[i] -= shift_0 - - shift_1 = (c_p1[i] - i_p0[i]) % dv[i] - c_p1[i] -= shift_1 - - # crop - crop_s = ((c_p0 - i_p0) / dv).astype(int) - crop_e = data.shape - ((i_p1 - c_p1) / dv).astype(int) - - data = data[crop_s[0]:crop_e[0], crop_s[1]:crop_e[1], crop_s[2]:crop_e[2]] - - origin = c_p0 - - new_cell = c_p1 - c_p0 - - # make new origin 0,0,0 - new_pos = pos - origin - - return data, np.diag(new_cell), new_pos - -for fn in glob("*.cube"): - filezip=fn+'.gz' - #if 'spin' in fn: - # filezip='./_spin_full.cube.gz' - - with open(fn, 'rb') as f: - file_lines = f.readlines() - file_str = "".join(file_lines) - - - numbers, positions, cell, origin, data = read_cube_file(file_lines) - - new_data, new_cell, new_pos = crop_cube(data, positions, cell, origin, x_crop=None, y_crop=3.5, z_crop=3.5) - - clip_data(new_data, absmin=1e-4) - - write_cube_file_gzip(filezip, numbers, new_pos, new_cell, new_data) - -EOF - -python ./cube_clipper_cropper.py -""" diff --git a/nanoribbon/workchains/nanoribbonwork.py b/nanoribbon/workchains/nanoribbonwork.py deleted file mode 100644 index 005fe64..0000000 --- a/nanoribbon/workchains/nanoribbonwork.py +++ /dev/null @@ -1,485 +0,0 @@ -import six -import numpy as np - -# AiiDA imports -from aiida.orm import Code, Computer, Dict, Int, Float, KpointsData, Str, StructureData, SinglefileData -from aiida.engine import WorkChain, ToContext, CalcJob, run, submit -#from aiida.orm.nodes.data.upf import get_pseudos_dict, get_pseudos_from_structure - -# aiida_quantumespresso imports -from aiida.engine import ExitCode -from aiida_quantumespresso.calculations.pw import PwCalculation -from aiida_quantumespresso.calculations.pp import PpCalculation -from aiida_quantumespresso.calculations.projwfc import ProjwfcCalculation -from aiida_quantumespresso.utils.pseudopotential import validate_and_prepare_pseudos_inputs - -# aditional imports -from . import aux_script_strings - - -class NanoribbonWorkChain(WorkChain): - - @classmethod - def define(cls, spec): - super(NanoribbonWorkChain, cls).define(spec) - spec.input("pw_code", valid_type=Code) - spec.input("pp_code", valid_type=Code) - spec.input("projwfc_code", valid_type=Code) - spec.input("structure", valid_type=StructureData) - spec.input("precision", valid_type=Float, default=Float(1.0), - required=False) - spec.input('pseudo_family', valid_type=Str, required=True, - help='An alternative to specifying the pseudo potentials manually in `pseudos`: one can specify the name ' - 'of an existing pseudo potential family and the work chain will generate the pseudos automatically ' - 'based on the input structure.') - # TODO: check why it does not work - #spec.inputs("metadata.label", valid_type=six.string_types, - # default="NanoribbonWorkChain", non_db=True, help="Label of the work chain.") - spec.outline( - cls.run_cell_opt1, - cls.run_cell_opt2, - cls.run_scf, - cls.run_export_hartree, - cls.run_bands, - cls.run_export_pdos, - cls.run_bands_lowres, - cls.run_export_orbitals, - cls.run_export_spinden, - ) - #spec.dynamic_output() - spec.outputs.dynamic = True - - # ========================================================================= - def run_cell_opt1(self): - structure = self.inputs.structure - return self._submit_pw_calc(structure, - label="cell_opt1", - runtype='vc-relax', - precision=0.5, - min_kpoints=int(1)) - - # ========================================================================= - def run_cell_opt2(self): - prev_calc = self.ctx.cell_opt1 - self._check_prev_calc(prev_calc) - structure = prev_calc.outputs.output_structure - return self._submit_pw_calc(structure, - label="cell_opt2", - runtype='vc-relax', - precision=1.0, - min_kpoints=int(1)) - - # ========================================================================= - def run_scf(self): - prev_calc = self.ctx.cell_opt2 - self._check_prev_calc(prev_calc) - structure = prev_calc.outputs.output_structure - return self._submit_pw_calc(structure, label="scf", runtype='scf', - precision=3.0, min_kpoints=int(10), wallhours=4) - - # ========================================================================= - def run_export_hartree(self): - self.report("Running pp.x to export hartree potential") - label = "export_hartree" - - builder = PpCalculation.get_builder() - builder.code = self.inputs.pp_code - - prev_calc = self.ctx.scf - self._check_prev_calc(prev_calc) - builder.parent_folder = prev_calc.outputs.remote_folder - - structure = prev_calc.inputs.structure - cell_a = structure.cell[0][0] - cell_b = structure.cell[1][1] - cell_c = structure.cell[2][2] - - builder.parameters = Dict(dict={ - 'inputpp': { - 'plot_num': 11, # the V_bare + V_H potential - }, - 'plot': { - 'iflag': 2, # 2D plot - # format suitable for gnuplot (2D) x, y, f(x,y) - 'output_format': 7, - # 3D vector, origin of the plane (in alat units) - 'x0(1)': 0.0, - 'x0(2)': 0.0, - 'x0(3)': cell_c/cell_a, - # 3D vectors which determine the plotting plane - # in alat units) - 'e1(1)': cell_a/cell_a, - 'e1(2)': 0.0, - 'e1(3)': 0.0, - 'e2(1)': 0.0, - 'e2(2)': cell_b/cell_a, - 'e2(3)': 0.0, - 'nx': 10, # Number of points in the plane - 'ny': 10, - 'fileout': 'vacuum_hartree.dat', - }, - }) - - builder.settings = Dict(dict={'additional_retrieve_list': ['vacuum_hartree.dat']}) - builder.metadata.label = label - builder.metadata.options = { - "resources": { - "num_machines": int(1), - }, - "max_wallclock_seconds": 1200 , - # workaround for flaw in PpCalculator. - # We don't want to retrive this huge intermediate file. - "append_text": u"rm -v aiida.filplot\n", - "withmpi": True, - } - - running = self.submit(builder) - return ToContext(**{label:running}) - - - # ========================================================================= - def run_bands(self): - prev_calc = self.ctx.scf - self._check_prev_calc(prev_calc) - structure = prev_calc.inputs.structure - parent_folder = prev_calc.outputs.remote_folder - return self._submit_pw_calc(structure, - label="bands", - parent_folder=parent_folder, - runtype='bands', - precision=4.0, - min_kpoints=int(20), - wallhours=6) - - # ========================================================================= - def run_bands_lowres(self): - prev_calc = self.ctx.scf - self._check_prev_calc(prev_calc) - structure = prev_calc.inputs.structure - parent_folder = prev_calc.outputs.remote_folder - return self._submit_pw_calc(structure, - label="bands_lowres", - parent_folder=parent_folder, - runtype='bands', - precision=0.0, - min_kpoints=int(12), - wallhours=2) - - # ========================================================================= - def run_export_orbitals(self): - self.report("Running pp.x to export KS orbitals") - builder = PpCalculation.get_builder() - builder.code = self.inputs.pp_code - nproc_mach = builder.code.computer.get_default_mpiprocs_per_machine() - - prev_calc = self.ctx.bands_lowres - self._check_prev_calc(prev_calc) - builder.parent_folder = prev_calc.outputs.remote_folder - - nel = prev_calc.res.number_of_electrons - nkpt = prev_calc.res.number_of_k_points - nbnd = prev_calc.res.number_of_bands - nspin = prev_calc.res.number_of_spin_components - volume = prev_calc.res.volume - kband1 = max(int(nel/2)-int(1), int(1)) - kband2 = min(int(nel/2)+int(2), int(nbnd)) - kpoint1 = int(1) - kpoint2 = int(nkpt * nspin) - nhours = int(2 + min(22, 2*int(volume/1500))) - - nnodes=int(prev_calc.attributes['resources']['num_machines']) - npools = int(prev_calc.inputs.settings['cmdline'][1]) - #nproc_mach=int(prev_calc.attributes['resources']['num_mpiprocs_per_machine']) - for inb in range(kband1,kband2+1): - builder.parameters = Dict(dict={ - 'inputpp': { - # contribution of a selected wavefunction - # to charge density - 'plot_num': 7, - 'kpoint(1)': kpoint1, - 'kpoint(2)': kpoint2, - 'kband(1)': inb, - 'kband(2)': inb, - }, - 'plot': { - 'iflag': 3, # 3D plot - 'output_format': 6, # CUBE format - 'fileout': '_orbital.cube', - }, - }) - - # commands to run after the main calculation is finished - append_text = u"" - # workaround for flaw in PpCalculator. - # We don't want to retrive this huge intermediate file. - append_text += u"rm -v aiida.filplot \n" - # Add the post-processing python scripts - #append_text += aux_script_strings.cube_cutter - append_text += aux_script_strings.cube_clipper_cropper - - builder.metadata.label = "export_orbitals" - builder.metadata.options = { - "resources": { - "num_machines": nnodes, - "num_mpiprocs_per_machine": nproc_mach, - }, - "max_wallclock_seconds": nhours * 60 * 60, # 6 hours - "append_text": append_text, - "withmpi": True, - } - - - builder.settings = Dict( - dict={'additional_retrieve_list': ['*.cube.gz'], - 'cmdline': - ["-npools", str(npools)] - } - ) - running = self.submit(builder) - label = 'export_orbitals_{}'.format(inb) - self.to_context(**{label:running}) - return - - # ========================================================================= - def run_export_spinden(self): - self.report("Running pp.x to compute spinden") - label = "export_spinden" - - builder = PpCalculation.get_builder() - builder.code = self.inputs.pp_code - nproc_mach = builder.code.computer.get_default_mpiprocs_per_machine() - prev_calc = self.ctx.scf - self._check_prev_calc(prev_calc) - builder.parent_folder = prev_calc.outputs.remote_folder - - nspin = prev_calc.res.number_of_spin_components - nnodes=int(prev_calc.attributes['resources']['num_machines']) - npools = int(prev_calc.inputs.settings.get_dict()['cmdline'][1]) - #nproc_mach=int(prev_calc.attributes['resources']['num_mpiprocs_per_machine']) - if nspin == 1: - self.report("Skipping, got only one spin channel") - return - - builder.parameters = Dict(dict={ - 'inputpp': { - 'plot_num': 6, # spin polarization (rho(up)-rho(down)) - - }, - 'plot': { - 'iflag': 3, # 3D plot - 'output_format': 6, # CUBE format - 'fileout': '_spin.cube', - }, - }) - # commands to run after the main calculation is finished - append_text = u"" - # workaround for flaw in PpCalculator. - # We don't want to retrive this huge intermediate file. - append_text += u"rm -v aiida.filplot \n" - # Add the post-processing python scripts - #append_text += aux_script_strings.cube_cutter - append_text += aux_script_strings.cube_clipper_cropper - - builder.metadata.label = label - builder.metadata.options = { - "resources": { - "num_machines": nnodes, - "num_mpiprocs_per_machine": nproc_mach, - }, - "max_wallclock_seconds": 30 * 60, # 30 minutes - "append_text": append_text, - "withmpi": True, - } - - builder.settings = Dict(dict={'additional_retrieve_list': ['*.cube.gz'], - 'cmdline': [ - "-npools", str(npools) - ] - }) - - future = self.submit(builder) - return ToContext(**{label:future}) - - # ========================================================================= - def run_export_pdos(self): - self.report("Running projwfc.x to export PDOS") - label = "export_pdos" - - builder = ProjwfcCalculation.get_builder() - builder.code = self.inputs.projwfc_code - prev_calc = self.ctx.bands - self._check_prev_calc(prev_calc) - - volume = prev_calc.res.volume - natoms=len(prev_calc.inputs.structure.attributes['sites']) - nproc_mach=4 - - if natoms < 60: - nnodes=int(2) - npools=int(2) - elif natoms < int(120): - nnodes=int(4) - npools=int(4) - else: - nnodes=int(prev_calc.attributes['resources']['num_machines']) - npools = int(prev_calc.inputs.settings.get_dict()['cmdline'][1]) - nproc_mach = builder.code.computer.get_default_mpiprocs_per_machine() - - nhours = 24 #2 + min(22, 2*int(volume/1500)) - builder.parent_folder = prev_calc.outputs.remote_folder - - # use the same number of pools as in bands calculation - builder.parameters = Dict(dict={ - 'projwfc': { - 'ngauss': 1, - 'degauss': 0.007, - 'DeltaE': 0.01, - 'filproj': 'projection.out', - }, - }) - - builder.metadata.label = label - builder.metadata.options = { - "resources": { - "num_machines": nnodes, - "num_mpiprocs_per_machine": nproc_mach, - }, - "max_wallclock_seconds": nhours * 60 * 60, # hours - "withmpi": True, - } - - builder.settings = Dict(dict={ - 'additional_retrieve_list': ['./out/aiida.save/*.xml', '*_up', '*_down', '*_tot'], - 'cmdline': ["-npools", str(npools)], - }) - - future = self.submit(builder) - return ToContext(**{label:future}) - - # ========================================================================= - def _check_prev_calc(self, prev_calc): - error = None - output_fname = prev_calc.attributes['output_filename'] - if not prev_calc.is_finished_ok: - error = "Previous calculation failed" #in state: "+prev_calc.get_state() - elif output_fname not in prev_calc.outputs.retrieved.list_object_names(): - error = "Previous calculation did not retrive {}".format(output_fname) - else: - content = prev_calc.outputs.retrieved.get_object_content(output_fname) - if "JOB DONE." not in content: - error = "Previous calculation not DONE." - if error: - self.report("ERROR: "+error) - #self.abort(msg=error) ## ABORT WILL NOT WORK, not defined - #raise Exception(error) - return ExitCode(450) - - # ========================================================================= - def _submit_pw_calc(self, structure, label, runtype, precision, - min_kpoints, wallhours=24, parent_folder=None): - self.report("Running pw.x for "+label) - builder = PwCalculation.get_builder() - - builder.code = self.inputs.pw_code - builder.structure = structure - builder.parameters = self._get_parameters(structure, runtype,label) - builder.pseudos = validate_and_prepare_pseudos_inputs(structure, None, self.inputs.pseudo_family) - - - if parent_folder: - builder.parent_folder = parent_folder - - # kpoints - cell_a = builder.structure.cell[0][0] - precision *= self.inputs.precision.value - nkpoints = max(min_kpoints, int(30 * 2.5/cell_a * precision)) - use_symmetry = runtype != "bands" - kpoints = self._get_kpoints(nkpoints, use_symmetry=use_symmetry) - builder.kpoints = kpoints - - # parallelization settings - ## TEMPORARY double pools in case of spin - spinpools=int(1) - start_mag = self._get_magnetization(structure) - if any([m != 0 for m in start_mag.values()]): - spinpools = int(2) - npools = spinpools*min( 1 + int(nkpoints/5), int(5) ) - natoms = len(structure.sites) - nnodes = (1 + int(natoms/60) ) * npools - - builder.metadata.label = label - builder.metadata.options = { - "resources": { "num_machines": nnodes }, - "withmpi": True, - "max_wallclock_seconds": wallhours * 60 * 60, - } - - builder.settings = Dict(dict={'cmdline': ["-npools", str(npools)]}) - - future = self.submit(builder) - return ToContext(**{label:future}) - - # ========================================================================= - def _get_parameters(self, structure, runtype, label): - params = {'CONTROL': { - 'calculation': runtype, - 'wf_collect': True, - 'forc_conv_thr': 0.0001, - 'nstep': 500, - }, - 'SYSTEM': { - 'ecutwfc': 50., - 'ecutrho': 400., - 'occupations': 'smearing', - 'degauss': 0.001, - }, - 'ELECTRONS': { - 'conv_thr': 1.e-8, - 'mixing_beta': 0.25, - 'electron_maxstep': 50, - 'scf_must_converge': False, - }, - } - - if label == 'cell_opt1': - params['CONTROL']['forc_conv_thr']=0.0005 - if runtype == "vc-relax": - # in y and z direction there is only vacuum - params['CELL'] = {'cell_dofree': 'x'} - - # if runtype == "bands": - # params['CONTROL']['restart_mode'] = 'restart' - - start_mag = self._get_magnetization(structure) - if any([m != 0 for m in start_mag.values()]): - params['SYSTEM']['nspin'] = 2 - params['SYSTEM']['starting_magnetization'] = start_mag - - return Dict(dict=params) - - # ========================================================================= - def _get_kpoints(self, nx, use_symmetry=True): - nx = max(1, nx) - - kpoints = KpointsData() - if use_symmetry: - kpoints.set_kpoints_mesh([nx, 1, 1], offset=[0.0, 0.0, 0.0]) - else: - # list kpoints explicitly - points = [[r, 0.0, 0.0] for r in np.linspace(0, 0.5, nx)] - kpoints.set_kpoints(points) - - return kpoints - - - # ========================================================================= - def _get_magnetization(self, structure): - start_mag = {} - for i in structure.kinds: - if i.name.endswith("1"): - start_mag[i.name] = 1.0 - elif i.name.endswith("2"): - start_mag[i.name] = -1.0 - else: - start_mag[i.name] = 0.0 - return start_mag diff --git a/pdos.ipynb b/pdos.ipynb index 1c98474..e7446df 100644 --- a/pdos.ipynb +++ b/pdos.ipynb @@ -13,15 +13,7 @@ "metadata": {}, "outputs": [], "source": [ - "%aiida" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ + "%aiida\n", "from urllib.parse import parse_qs, urlsplit\n", "from nanoribbon.viewers import NanoribbonPDOSWidget" ] @@ -33,8 +25,7 @@ "outputs": [], "source": [ "id = parse_qs(urlsplit(jupyter_notebook_url).query)['id'][0]\n", - "nanoribon_viewer = NanoribbonPDOSWidget(load_node(int(id)))\n", - "#nanoribon_viewer = NanoribbonPDOSWidget(load_node(int(4692)))" + "nanoribon_viewer = NanoribbonPDOSWidget(load_node(int(id)))" ] }, { diff --git a/show.ipynb b/show.ipynb index 49ecc00..c04e5b5 100644 --- a/show.ipynb +++ b/show.ipynb @@ -13,7 +13,10 @@ "metadata": {}, "outputs": [], "source": [ - "%aiida" + "%%javascript\n", + "IPython.OutputArea.prototype._should_scroll = function(lines) {\n", + " return false;\n", + "}" ] }, { @@ -22,6 +25,7 @@ "metadata": {}, "outputs": [], "source": [ + "%aiida\n", "from urllib.parse import parse_qs, urlsplit\n", "from nanoribbon.viewers import NanoribbonShowWidget\n", "import ipywidgets as ipw" @@ -37,32 +41,13 @@ "nanoribon_viewer = NanoribbonShowWidget(load_node(int(id)))" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "display(nanoribon_viewer)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Spin Density" - ] - }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "layout = ipw.Layout(width='auto')\n", - "display(ipw.HBox([nanoribon_viewer.spindensity_2d(), nanoribon_viewer.spindensity_3d()], layout=layout))" + "display(nanoribon_viewer)" ] } ], diff --git a/submit.ipynb b/submit.ipynb index 0a151a0..72d0ec9 100644 --- a/submit.ipynb +++ b/submit.ipynb @@ -13,35 +13,25 @@ "metadata": {}, "outputs": [], "source": [ - "%aiida" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# General imports\n", + "# General imports.\n", "import numpy as np\n", "import ipywidgets as ipw\n", - "from collections import OrderedDict\n", - "from ase.data import vdw_radii\n", - "from IPython.display import display, clear_output, HTML\n", - "import itertools\n", + "from IPython.display import clear_output\n", "\n", - "# AiiDA & AiiDA lab imports\n", - "from aiida.orm import Str,StructureData\n", + "# AiiDA & AiiDA lab imports.\n", + "%aiida\n", "from aiida.engine import calcfunction\n", - "from aiida.orm import SinglefileData\n", "from aiidalab_widgets_base.utils import string_range_to_list, list_to_string_range\n", "\n", - "from nanoribbon.viewers import Smiles2GNRWidget\n", + "# Local imports.\n", + "from nanoribbon.viewers import Smiles2GnrWidget\n", "from aiidalab_widgets_base import CodeDropdown, StructureManagerWidget,BasicStructureEditor, StructureBrowserWidget, StructureUploadWidget, SubmitButtonWidget, SmilesWidget\n", "from aiida.engine import submit\n", "\n", - "# Work Chains\n", - "NanoribbonWorkChain = WorkflowFactory('nanoribbon')" + "# Work chains, data objects.\n", + "SinglefileData = DataFactory('singlefile')\n", + "StructureData = DataFactory('structure')\n", + "NanoribbonWorkChain = WorkflowFactory('nanotech_empa.nanoribbon')" ] }, { @@ -50,12 +40,16 @@ "metadata": {}, "outputs": [], "source": [ - "structure_selector = StructureManagerWidget(importers=[\n", - " (\"SMILES2GNR\", Smiles2GNRWidget()),\n", - " (\"Import from computer\", StructureUploadWidget()),\n", - " (\"AiiDA database\", StructureBrowserWidget()),\n", - " ],editors = [(\"Edit structure\", BasicStructureEditor())],\n", - " storable=False,node_class='StructureData')\n", + "structure_selector = StructureManagerWidget(\n", + " importers=[\n", + " StructureUploadWidget(title=\"From computer\"),\n", + " StructureBrowserWidget(title=\"AiiDA database\"),\n", + " Smiles2GnrWidget(),\n", + "\n", + " ],\n", + " editors = [BasicStructureEditor()],\n", + " storable=False,\n", + " node_class='StructureData')\n", "display(structure_selector)" ] },