From 79ef46b0681c32a77e90cbd019d8f67c4712ccf0 Mon Sep 17 00:00:00 2001 From: yakutovicha Date: Fri, 12 Apr 2019 13:40:30 +0200 Subject: [PATCH 1/3] Add semi-automated computer and code setup (#12) * `SshComputerSetup` widget 1) Configures ssh access to a computer (with or without proxy) 2) Allows to provide initial arguments' values at the time of object creation: sshcomputer = SshComputerSetup(hostname='myhost.ch') 3) Contains `setup_counter` traitlet that can be observed by other widgets: ``` aiidacomputer = AiidaComputerSetup(**args) sshcomputer.observe(aiidacomputer.get_available_computers, names=['setup_counter']) ``` 4) Can parse the URL parameters and insert them into the object: ``` args = extract_sshcomputersetup_arguments(parsed_url) sshcomputer = SshComputerSetup(**args) ``` * `AiidaComputerSetup` widget 1) Sets up an AiiDA computer in semi-automated way and tests it 2) Looks at already configured ssh-hosts and take ssh connection information from `~/.ssh/config` file 3) Automatically loads available transport types and schedulers 4) Allows to provide initial arguments' values at the time of object creation: ``` aiidacomputer = AiidaComputerSetup(hostname='myhost.ch') ``` 5) Can parse the URL parameters and insert them into object: ``` parsed_url = urlparse.parse_qs(urlparse.urlsplit(jupyter_notebook_url).query) args = extract_aiidacomputer_arguments(parsed_url) aiidacomputer = AiidaComputerSetup(**args) ``` * `ComputerDropdown` widget 1) Provides a list of selected configured AiiDA computer(s) 2) Has a link to the computer setup notebook * `AiiDACodeSetup` widget 1) Semi-automated widget to configure AiiDA code 2) Employs `ComputerDropdown` to chose a configured AiiDA computer 3) Allows to provide initial arguments' values at the time of object creation: `code = AiiDACodeSetup(label='cp2k')` 4) Can parse the URL parameters and insert them into object: ``` parsed_url = urlparse.parse_qs(urlparse.urlsplit(jupyter_notebook_url).query) args = extract_aiidacodesetup_arguments(parsed_url) code = AiiDACodeSetup(**args) ``` 5) Add an observe function that updates available computer list on every change of SshComputerSetup object: `sshcomputer.observe(aiidacomputer.get_available_computers, names=['setup_counter'])` * Modification of `CodeDropdown` widget: 1) Has a link to the code setup notebook * Modification of module imports and dbenv checks: 1) Move dbenv-related checks into the module top `__init__.py` file and remove them from all other files 2) Add new Code- and Computer-setup imports in the module top __init__.py file --- aiidalab_widgets_base/__init__.py | 9 +- aiidalab_widgets_base/codes.py | 193 +++++- aiidalab_widgets_base/computers.py | 717 ++++++++++++++++++++++ aiidalab_widgets_base/search_qb.py | 5 - aiidalab_widgets_base/structures.py | 7 +- aiidalab_widgets_base/structures_multi.py | 6 - cod.ipynb | 2 +- codes.ipynb | 2 +- miscellaneous/images/computer.png | Bin 0 -> 94771 bytes setup_code.ipynb | 62 ++ setup_computer.ipynb | 91 +++ start.py | 8 +- 12 files changed, 1071 insertions(+), 31 deletions(-) create mode 100644 aiidalab_widgets_base/computers.py create mode 100644 miscellaneous/images/computer.png create mode 100644 setup_code.ipynb create mode 100644 setup_computer.ipynb diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index e1dd90bcb..e06ce9d2f 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -1,6 +1,13 @@ # pylint: disable=unused-import +from aiida import load_dbenv, is_dbenv_loaded +from aiida.backends import settings +if not is_dbenv_loaded(): + load_dbenv(profile=settings.AIIDADB_PROFILE) + from .structures import StructureUploadWidget # noqa from .structures_multi import MultiStructureUploadWidget # noqa -from .codes import CodeDropdown # noqa +from .codes import CodeDropdown, AiiDACodeSetup, extract_aiidacodesetup_arguments # noqa +from .computers import SshComputerSetup, extract_sshcomputersetup_arguments # noqa +from .computers import AiidaComputerSetup, extract_aiidacomputer_arguments # noqa __version__ = "0.2.0a1" diff --git a/aiidalab_widgets_base/codes.py b/aiidalab_widgets_base/codes.py index 115f95d15..46d7eef1f 100644 --- a/aiidalab_widgets_base/codes.py +++ b/aiidalab_widgets_base/codes.py @@ -2,8 +2,28 @@ from __future__ import absolute_import import ipywidgets as ipw + +from subprocess import check_output from IPython.display import clear_output +from aiida.orm import Code + +VALID_AIIDA_CODE_SETUP_ARGUMETNS = {'label', 'selected_computer', 'plugin', 'description', + 'exec_path', 'prepend_text', 'append_text'} + +def valid_arguments(arguments, valid_arguments): + result = {} + for key, value in arguments.items(): + if key in valid_arguments: + if type(value) is tuple or type(value) is list: + result[key] = '\n'.join(value) + else: + result[key] = value + return result + +def extract_aiidacodesetup_arguments(arguments): + return valid_arguments(arguments, VALID_AIIDA_CODE_SETUP_ARGUMETNS) + class CodeDropdown(ipw.VBox): def __init__(self, input_plugin, text='Select code:', **kwargs): @@ -18,25 +38,23 @@ def __init__(self, input_plugin, text='Select code:', **kwargs): self.input_plugin = input_plugin self.codes = {} - self.label = ipw.Label(value=text) - self.dropdown = ipw.Dropdown(options=[], disabled=True) + self.dropdown = ipw.Dropdown(description=text, disabled=True) + self._btn_refresh = ipw.Button(description="Refresh", layout=ipw.Layout(width="70px")) + self._btn_refresh.on_click(self.refresh) + self._setup_another = ipw.HTML(value="""Setup new code""") self.output = ipw.Output() - children = [ipw.HBox([self.label, self.dropdown, self.output])] + children = [ipw.HBox([self.dropdown, self._btn_refresh, self._setup_another]), + self.output] super(CodeDropdown, self).__init__(children=children, **kwargs) - from aiida import load_dbenv, is_dbenv_loaded - from aiida.backends import settings - if not is_dbenv_loaded(): - load_dbenv(profile=settings.AIIDADB_PROFILE) self.refresh() def _get_codes(self, input_plugin): from aiida.orm.querybuilder import QueryBuilder - from aiida.orm import Code, Computer from aiida.backends.utils import get_automatic_user - + from aiida.orm import Computer current_user = get_automatic_user() qb = QueryBuilder() @@ -62,7 +80,7 @@ def _get_codes(self, input_plugin): codes = {"{}@{}".format(r[1].label, r[0].name): r[1] for r in results} return codes - def refresh(self): + def refresh(self, b=None): with self.output: clear_output() self.codes = self._get_codes(self.input_plugin) @@ -83,3 +101,158 @@ def selected_code(self): return self.codes[self.dropdown.value] except KeyError: return None + +class AiiDACodeSetup(ipw.VBox): + """Class that allows to setup AiiDA code""" + def __init__(self, **kwargs): + from aiida.common.pluginloader import all_plugins + from aiidalab_widgets_base.computers import ComputerDropdown + + style = {"description_width":"200px"} + + # list of widgets to be displayed + + self._inp_code_label = ipw.Text(description="AiiDA code label:", + layout=ipw.Layout(width="500px"), + style=style) + + self._computer = ComputerDropdown(layout={'margin': '0px 0px 0px 125px'}) + + self._inp_code_description = ipw.Text(placeholder='No description (yet)', + description="Code description:", + layout=ipw.Layout(width="500px"), + style=style) + + self._inp_code_plugin = ipw.Dropdown(options=sorted(all_plugins('calculations')), + description="Code plugin:", + layout=ipw.Layout(width="500px"), + style=style) + + self._exec_path = ipw.Text(placeholder='/path/to/executable', + description="Absolute path to executable:", + layout=ipw.Layout(width="500px"), + style=style) + + self._prepend_text = ipw.Textarea(placeholder='Text to prepend to each command execution', + description='Prepend text:', + layout=ipw.Layout(width="400px")) + + self._append_text = ipw.Textarea(placeholder='Text to append to each command execution', + description='Append text:', + layout=ipw.Layout(width="400px")) + + self._btn_setup_code = ipw.Button(description="Setup code") + self._btn_setup_code.on_click(self._setup_code) + self._setup_code_out = ipw.Output() + children = [ipw.HBox([ipw.VBox([self._inp_code_label, + self._computer, + self._inp_code_plugin, + self._inp_code_description, + self._exec_path]), ipw.VBox([self._prepend_text, + self._append_text])]), + self._btn_setup_code, + self._setup_code_out, + ] + # Check if some settings were already provided + self._predefine_settings(**kwargs) + super(AiiDACodeSetup, self).__init__(children, **kwargs) + + def _predefine_settings(self, **kwargs): + for key, value in kwargs.items(): + if hasattr(self, key): + setattr(self, key, value) + else: + raise AttributeError("'{}' object has no attribute '{}'".format(self, key)) + + def _setup_code(self, b=None): + with self._setup_code_out: + clear_output() + if self.label is None: + print("You did not specify code label") + return + if not self.exec_path: + print("You did not specify absolute path to the executable") + return + if self.exists(): + print ("Code {}@{} already exists".format(self.label, self.selected_computer.name)) + return + code = Code(remote_computer_exec=(self.selected_computer, self.exec_path)) + code.label = self.label + code.description = self.description + code.set_input_plugin_name(self.plugin) + code.set_prepend_text(self.prepend_text) + code.set_append_text(self.append_text) + code.store() + code._reveal() + full_string = "{}@{}".format(self.label, self.selected_computer.name) + print(check_output(['verdi', 'code', 'show', full_string])) + + def exists(self): + from aiida.common.exceptions import NotExistent, MultipleObjectsError + try: + Code.get_from_string("{}@{}".format(self.label, self.selected_computer.name)) + return True + except MultipleObjectsError: + return True + except NotExistent: + return False + + @property + def label(self): + if len(self._inp_code_label.value.strip()) == 0: + return None + else: + return self._inp_code_label.value + + @label.setter + def label(self, label): + self._inp_code_label.value = label + + @property + def description(self): + return self._inp_code_description.value + + @description.setter + def description(self, description): + self._inp_code_description.value = description + + @property + def plugin(self): + return self._inp_code_plugin.value + + @plugin.setter + def plugin(self, plugin): + if plugin in self._inp_code_plugin.options: + self._inp_code_plugin.value = plugin + + @property + def exec_path(self): + return self._exec_path.value + + @exec_path.setter + def exec_path(self, exec_path): + self._exec_path.value = exec_path + + @property + def prepend_text(self): + return self._prepend_text.value + + @prepend_text.setter + def prepend_text(self, prepend_text): + self._prepend_text.value = prepend_text + + @property + def append_text(self): + return self._append_text.value + + @append_text.setter + def append_text(self, append_text): + self._append_text.value = append_text + + @property + def selected_computer(self): + return self._computer.selected_computer + + @selected_computer.setter + def selected_computer(self, selected_computer): + self._computer.selected_computer = selected_computer diff --git a/aiidalab_widgets_base/computers.py b/aiidalab_widgets_base/computers.py new file mode 100644 index 000000000..96cb55eb8 --- /dev/null +++ b/aiidalab_widgets_base/computers.py @@ -0,0 +1,717 @@ +from __future__ import print_function + +import pexpect +import ipywidgets as ipw + +from os import path +from copy import copy +from IPython.display import clear_output +from subprocess import check_output, call +from traitlets import Int + +from aiida.orm import Computer +from aiida.backends.utils import get_automatic_user, get_backend_type +from aiida.common.exceptions import NotExistent +from aiida.transport.plugins.ssh import parse_sshconfig + +if get_backend_type() == 'sqlalchemy': + from aiida.backends.sqlalchemy.models.authinfo import DbAuthInfo +else: + from aiida.backends.djsite.db.models import DbAuthInfo + +VALID_SSH_COMPUTER_SETUP_ARGUMETNS = {'hostname', 'username', 'proxy_hostname', 'proxy_username'} +VALID_AIIDA_COMPUTER_SETUP_ARGUMETNS = {'name', 'hostname', 'description', 'workdir', 'mpirun_cmd', + 'ncpus', 'transport_type', 'scheduler', 'prepend_text', 'append_text'} + +def valid_arguments(arguments, valid_arguments): + result = {} + for key, value in arguments.items(): + if key in valid_arguments: + if type(value) is tuple or type(value) is list: + result[key] = '\n'.join(value) + else: + result[key] = value + return result + +def extract_sshcomputersetup_arguments(arguments): + return valid_arguments(arguments, VALID_SSH_COMPUTER_SETUP_ARGUMETNS) + +def extract_aiidacomputer_arguments(arguments): + return valid_arguments(arguments, VALID_AIIDA_COMPUTER_SETUP_ARGUMETNS) + +class SshComputerSetup(ipw.VBox): + setup_counter = Int(0) # Traitlet to inform other widgets about changes + def __init__(self, **kwargs): + style = {"description_width":"200px"} + computer_image = ipw.HTML('') + self._inp_username = ipw.Text(description="SSH username:", + layout=ipw.Layout(width="350px"), + style=style) + self._inp_password = ipw.Password(description="SSH password:", + layout=ipw.Layout(width="130px"), + style=style) + # Computer ssh settings + self._inp_computer_hostname = ipw.Text(description="Computer name:", + layout=ipw.Layout(width="350px"), + style=style) + self._use_proxy = ipw.Checkbox(value=False, description='Use proxy') + self._use_proxy.observe(self.on_use_proxy_change, names='value') + + + # Proxy ssh settings + self._inp_proxy_address = ipw.Text(description="Proxy server address:", + layout=ipw.Layout(width="350px"), + style=style) + self._use_diff_proxy_username = ipw.Checkbox(value=False, + description='Use different username and password', + layout={'width': 'initial'}) + self._use_diff_proxy_username.observe(self.on_use_diff_proxy_username_change, names='value') + + self._inp_proxy_username = ipw.Text(value='', + description="Proxy server username:", + layout=ipw.Layout(width="350px"), style=style) + self._inp_proxy_password = ipw.Password(value='', + description="Proxy server password:", + layout=ipw.Layout(width="138px"), + style=style) + self._btn_setup_ssh = ipw.Button(description="Setup ssh") + self._btn_setup_ssh.on_click(self.on_setup_ssh) + self._setup_ssh_out = ipw.Output() + + # Check if some settings were already provided + self._predefine_settings(**kwargs) + + # Defining widgets positions + computer_ssh_box = ipw.VBox([self._inp_computer_hostname, + self._inp_username, + self._inp_password, + self._use_proxy], + layout=ipw.Layout(width="400px")) + + self._proxy_user_password_box = ipw.VBox([self._inp_proxy_username, + self._inp_proxy_password], + layout={'visibility':'hidden'}) + + self._proxy_ssh_box = ipw.VBox([self._inp_proxy_address, + self._use_diff_proxy_username, + self._proxy_user_password_box], + layout = {'visibility':'hidden','width':'400px'}) + + children = [ipw.HBox([computer_image, computer_ssh_box, self._proxy_ssh_box]), + self._btn_setup_ssh, + self._setup_ssh_out] + + super(SshComputerSetup, self).__init__(children, **kwargs) + + def _predefine_settings(self, **kwargs): + for key, value in kwargs.items(): + if hasattr(self, key): + setattr(self, key, value) + else: + raise AttributeError("'{}' object has no attirubte '{}'".format(self, key)) + + def _ssh_keygen(self): + fn = path.expanduser("~/.ssh/id_rsa") + if not path.exists(fn): + print("Creating ssh key pair") + # returns non-0 if the key pair already exists + call(["ssh-keygen", "-f", fn, "-t", "rsa", "-N", ""]) + + def is_host_known(self, hostname=None): + if hostname is None: + hostname = self.hostname + fn = path.expanduser("~/.ssh/known_hosts") + if not path.exists(fn): + return False + return call(["ssh-keygen", "-F", hostname]) == 0 + + def _make_host_known(self, hostname, proxycmd=[]): + fn = path.expanduser("~/.ssh/known_hosts") + print("Adding keys from %s to %s"%(hostname, fn)) + hashes = check_output(proxycmd+["ssh-keyscan", "-H", hostname]) + with open(fn, "a") as f: + f.write(hashes) + + def can_login(self, silent=False): + if self.username is None: # if I can't find the username - I must fail + return False + userhost = self.username+"@"+self.hostname + if not silent: + print("Trying ssh "+userhost+"... ", end='') + # With BatchMode on, no password prompt or other interaction is attempted, + # so a connect that requires a password will fail. + ret = call(["ssh", userhost, "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "true"]) + if not silent: + print("Ok" if ret==0 else "Failed") + return ret==0 + + def is_in_config(self): + fn = path.expanduser("~/.ssh/config") + if not path.exists(fn): + return False + cfglines = open(fn).read().split("\n") + return "Host "+self.hostname in cfglines + + def _write_ssh_config(self, proxycmd=''): + fn = path.expanduser("~/.ssh/config") + print("Adding section to "+fn) + with open(fn, "a") as f: + f.write("Host "+self.hostname+"\n") + f.write("User "+self.username+"\n") + if proxycmd: + f.write("ProxyCommand ssh -q -Y "+proxycmd+" netcat %h %p\n") + f.write("ServerAliveInterval 5\n") + + def _send_pubkey(self, hostname, username, password, proxycmd=''): + from pexpect import TIMEOUT + timeout = 10 + print("Sending public key to {}... ".format(hostname),end='') + str_ssh = 'ssh-copy-id {}@{}'.format(username, hostname) + if proxycmd: + str_ssh += ' -o "ProxyCommand ssh -q -Y '+proxycmd+' netcat %h %p\n"' + child = pexpect.spawn(str_ssh) + try: + index = child.expect(['s password:', # 0 + 'ERROR: No identities found', # 1 + 'All keys were skipped because they already exist on the remote system', # 2 + 'Could not resolve hostname', # 3 + pexpect.EOF],timeout=timeout) # final + except TIMEOUT: + print ("Exceeded {} s timeout".format(timeout)) + return False + + if index == 0: + child.sendline(password) + try: + child.expect("Now try logging into",timeout=5) + except: + print("Failed") + print("Please check your username and/or password") + return False + print("Ok") + child.close() + return True + elif index == 1: + print("Failed") + print("Looks like the key pair is not present in ~/.ssh folder") + return False + elif index == 2: + print("Keys are already there") + return True + elif index == 3: + print("Failed") + print("Unknown hostname") + return False + else: + print ("Failed") + print ("Unknown problem") + print (child.before, child.after) + child.close() + return False + + def _configure_proxy(self, password, proxy_password): + # if proxy IS required + if self._use_proxy.value: + # again some standard checks if proxy server parameters are provided + if self.proxy_hostname is None: # hostname + print("Please specify the proxy server hostname") + return False, '' + # if proxy username and password must be different from the main computer - they should be provided + if self._use_diff_proxy_username.value: + # check username + if not self.proxy_username is None: + proxy_username = self.proxy_username + else: + print("Please specify the proxy server username") + return False, '' + # check password + if len(proxy_password.strip()) == 0: + print("Please specify the proxy server password") + return False, '' + else: # if username and password are the same as for the main computer + proxy_username = self.username + proxy_password = password + # make proxy server known + if not self.is_host_known(self.proxy_hostname): + self._make_host_known(self.proxy_hostname) + # Finally trying to connect + if self._send_pubkey(self.proxy_hostname, proxy_username, proxy_password): + return True, proxy_username + '@' + self.proxy_hostname + else: + print ("Could not send public key to {} (proxy server).".format(self.proxy_hostname)) + return False, '' + # if proxy is NOT required + else: + return True, '' + + def on_setup_ssh(self, b): + """ATTENTION: modifying the order of operations in this function can lead to unexpected problems""" + with self._setup_ssh_out: + clear_output() + self._ssh_keygen() + + #temporary passwords + password = self.__password + proxy_password = self.__proxy_password + + # step 1: if hostname is not provided - do not do anything + if self.hostname is None: # check hostname + print("Please specify the computer hostname") + return + + # step 2: check if password-free access was enabled earlier + if self.can_login(): + print ("Password-free access is already enabled") + # it can still happen that password-free access is enabled + # but host is not present in the config file - fixing this + if not self.is_in_config(): + self._write_ssh_config() # we do not use proxy here, because if computer + # can be accessed without any info in the config - proxy is not needed. + self.setup_counter += 1 # only if config file has changed - increase setup_counter + return + + # step 3: if can't login already, chek whether all required information is provided + if self.username is None: # check username + print("Please enter your ssh username") + return + if len(password.strip()) == 0: # check password + print("Please enter your ssh password") + return + + # step 4: get the right commands to access the proxy server (if provided) + success, proxycmd = self._configure_proxy(password, proxy_password) + if not success: + return + + # step 5: make host known by ssh on the proxy server + if not self.is_host_known(): + self._make_host_known(self.hostname,['ssh']+[proxycmd] if proxycmd else []) + + # step 6: sending public key to the main host + if not self._send_pubkey(self.hostname, self.username, password, proxycmd): + print ("Could not send public key to {}".format(self.hostname)) + return + + # step 7: modify the ssh config file if necessary + if not self.is_in_config(): + self._write_ssh_config(proxycmd=proxycmd) + # TODO: add a check if new config is different from the current one. If so + # infrom the user about it. + + # step 8: final check + if self.can_login(): + self.setup_counter += 1 + print("Automatic ssh setup successful :-)") + return + else: + print("Automatic ssh setup failed, sorry :-(") + return + + def on_use_proxy_change(self, b): + if self._use_proxy.value: + self._proxy_ssh_box.layout.visibility = 'visible' + else: + self._proxy_ssh_box.layout.visibility = 'hidden' + self._use_diff_proxy_username.value = False + + def on_use_diff_proxy_username_change(self, b): + if self._use_diff_proxy_username.value: + self._proxy_user_password_box.layout.visibility = 'visible' + else: + self._proxy_user_password_box.layout.visibility = 'hidden' + +# Keep this function only because it might be used later. +# What it does: looks inside .ssh/config file and loads computer setup from +# there (if present) +# def _get_from_config(self, b): +# config = parse_sshconfig(self.hostname) +# if 'user' in config: +# self._inp_username.value = config['user'] +# else: +# self._inp_username.value = '' +# if 'proxycommand' in config: +# self._use_proxy.value = True +# proxy = ''.join([ s for s in config['proxycommand'].split() if '@' in s]) +# username, hostname = proxy.split('@') +# self._inp_proxy_address.value = hostname +# if username != self.username: +# self._use_diff_proxy_username.value = True +# self.proxy_username = username +# else: +# self._use_proxy.value = False + + @property + def __password(self): + """Returning the password and immediately destroying it""" + passwd = copy(self._inp_password.value) + self._inp_password.value = '' + return passwd + + @property + def __proxy_password(self): + """Returning the password and immediately destroying it""" + passwd = copy(self._inp_proxy_password.value) + self._inp_proxy_password.value = '' + return passwd + + @property + def hostname(self): + if len(self._inp_computer_hostname.value.strip()) == 0: # check hostname + return None + else: + return self._inp_computer_hostname.value + + @hostname.setter + def hostname(self, hostname): + self._inp_computer_hostname.value = hostname + + @property + def username(self): + """Loking for username in user's input and config file""" + if len(self._inp_username.value.strip()) == 0: # if username provided by user + if not self.hostname is None: + config = parse_sshconfig(self.hostname) + if 'user' in config: # if username is present in the config file + return config['user'] + else: + return None + else: + return self._inp_username.value + + @username.setter + def username(self, username): + self._inp_username.value = username + + @property + def proxy_hostname(self): + if len(self._inp_proxy_address.value.strip()) == 0: + return None + else: + return self._inp_proxy_address.value + + @proxy_hostname.setter + def proxy_hostname(self, proxy_hostname): + self._use_proxy.value = True + self._inp_proxy_address.value = proxy_hostname + + @property + def proxy_username(self): + if len(self._inp_proxy_username.value.strip()) == 0: + return None + else: + return self._inp_proxy_username.value + + @proxy_username.setter + def proxy_username(self, proxy_username): + self._use_proxy.value = True + self._use_diff_proxy_username.value = True + self._inp_proxy_username.value = proxy_username + +class AiidaComputerSetup(ipw.VBox): + def __init__(self, **kwargs): + from aiida.transport import Transport + from aiida.scheduler import Scheduler + style = {"description_width":"200px"} + + # list of widgets to be displayed + self._btn_setup_comp = ipw.Button(description="Setup computer") + self._btn_setup_comp.on_click(self._on_setup_computer) + self._inp_computer_name = ipw.Text(value='', + placeholder='Will only be used within AiiDA', + description="AiiDA computer name:", + layout=ipw.Layout(width="500px"), + style=style) + self._computer_hostname = ipw.Dropdown(description="Select among configured hosts:", + layout=ipw.Layout(width="500px"), + style=style) + self._inp_computer_description = ipw.Text(value='', + placeholder='No description (yet)', + description="Computer description:", + layout=ipw.Layout(width="500px"), + style=style) + self._computer_workdir = ipw.Text(value='/scratch/{username}/aiida_run', + description="Workdir:", + layout=ipw.Layout(width="500px"), + style=style) + self._computer_mpirun_cmd = ipw.Text(value='mpirun -n {tot_num_mpiprocs}', + description="Mpirun command:", + layout=ipw.Layout(width="500px"), + style=style) + self._computer_ncpus = ipw.IntText(value=12, + step=1, + description='Number of CPU(s) per node:', + layout=ipw.Layout(width="270px"), + style=style) + self._transport_type = ipw.Dropdown(value='ssh', + options=Transport.get_valid_transports(), + description="Transport type:", + style=style) + self._scheduler = ipw.Dropdown(value='slurm', + options=Scheduler.get_valid_schedulers(), + description="Scheduler:", + style=style) + self._prepend_text = ipw.Textarea(placeholder='Text to prepend to each command execution', + description='Prepend text:', + layout=ipw.Layout(width="400px") + ) + self._append_text = ipw.Textarea(placeholder='Text to append to each command execution', + description='Append text:', + layout=ipw.Layout(width="400px") + ) + self._btn_test = ipw.Button(description="Test computer") + self._btn_test.on_click(self.test) + + self._setup_comp_out = ipw.Output(layout=ipw.Layout(width="500px")) + self._test_out = ipw.Output(layout=ipw.Layout(width="500px")) + + # getting the list of available computers + self.get_available_computers() + + # Check if some settings were already provided + self._predefine_settings(**kwargs) + children =[ipw.HBox([ipw.VBox([self._inp_computer_name, + self._computer_hostname, + self._inp_computer_description, + self._computer_workdir, + self._computer_mpirun_cmd, + self._computer_ncpus, + self._transport_type, + self._scheduler]), + ipw.VBox([self._prepend_text, + self._append_text])]), + ipw.HBox([self._btn_setup_comp, self._btn_test]), + ipw.HBox([self._setup_comp_out, self._test_out]), + ] + super(AiidaComputerSetup, self).__init__(children, **kwargs) + + def _predefine_settings(self, **kwargs): + for key, value in kwargs.items(): + if hasattr(self, key): + setattr(self, key, value) + else: + raise AttributeError("'{}' object has no attribute '{}'".format(self, key)) + + def get_available_computers(self, b=None): + fn = path.expanduser("~/.ssh/config") + if not path.exists(fn): + return [] + cfglines = open(fn).readlines() + self._computer_hostname.options = [line.split()[1] for line in cfglines if 'Host' in line] + + def _configure_computer(self): + """create DbAuthInfo""" + print("Configuring '{}'".format(self.name)) + sshcfg = parse_sshconfig(self.hostname) + authparams = { + 'compress': True, + 'gss_auth': False, + 'gss_deleg_creds': False, + 'gss_host': self.hostname, + 'gss_kex': False, + 'key_policy': 'WarningPolicy', + 'load_system_host_keys': True, + 'port': 22, + 'timeout': 60, + } + if 'user' in sshcfg: + authparams['username'] = sshcfg['user'] + else: + print ("SSH username is not provided, please run `verdi computer configure {}` " + "from the command line".format(self.name)) + return + if 'proxycommand' in sshcfg: + authparams['proxy_command'] = sshcfg['proxycommand'] + aiidauser = get_automatic_user() + authinfo = DbAuthInfo(dbcomputer=Computer.get(self.name).dbcomputer, aiidauser=aiidauser) + authinfo.set_auth_params(authparams) + authinfo.save() + print(check_output(['verdi', 'computer', 'show', self.name])) + + def _on_setup_computer(self, b): + with self._setup_comp_out: + clear_output() + if self.name is None: # check hostname + print("Please specify the computer name (for AiiDA)") + return + try: + computer = Computer.get(self.name) + print("A computer called {} already exists.".format(self.name)) + return + except NotExistent: + pass + + print("Creating new computer with name '{}'".format(self.name)) + computer = Computer(name=self.name) + computer.set_hostname(self.hostname) + computer.set_description(self.description) + computer.set_enabled_state(True) + computer.set_transport_type(self.transport_type) + computer.set_scheduler_type(self.scheduler) + computer.set_workdir(self.workdir) + computer.set_mpirun_command(self.mpirun_cmd.split()) + computer.set_default_mpiprocs_per_machine(self.ncpus) + if self._prepend_text.value: + computer.set_prepend_text(self.prepend_text) + if self._append_text.value: + computer.set_append_text(self.append_text) + computer.store() + self._configure_computer() + + def test(self, b=None): + with self._test_out: + clear_output() + print(check_output(['verdi', 'computer', 'test', '--traceback', self.name])) + + @property + def name(self): + if len(self._inp_computer_name.value.strip()) == 0: # check hostname + return None + else: + return self._inp_computer_name.value + + @name.setter + def name(self, name): + self._inp_computer_name.value = name + + @property + def hostname(self): + if self._computer_hostname.value is None or len(self._computer_hostname.value.strip()) == 0: # check hostname + return None + else: + return self._computer_hostname.value + + @hostname.setter + def hostname(self, hostname): + if hostname in self._computer_hostname.options: + self._computer_hostname.value = hostname + + @property + def description(self): + return self._inp_computer_description.value + + @description.setter + def description(self, description): + self._inp_computer_description.value = description + + @property + def workdir(self): + return self._computer_workdir.value + + @workdir.setter + def workdir(self, workdir): + self._computer_workdir.value = workdir + + @property + def mpirun_cmd(self): + return self._computer_mpirun_cmd.value + + @mpirun_cmd.setter + def mpirun_cmd(self, mpirun_cmd): + self._computer_mpirun_cmd.value = mpirun_cmd + + @property + def ncpus(self): + return self._computer_ncpus.value + + @ncpus.setter + def ncpus(self, ncpus): + self._computer_ncpus.value = int(ncpus) + + @property + def transport_type(self): + return self._transport_type.value + + @transport_type.setter + def transport_type(self, transport_type): + if transport_type in self._transport_type.options: + self._transport_type.value = transport_type + @property + def scheduler(self): + return self._scheduler.value + + @scheduler.setter + def scheduler(self, scheduler): + if scheduler in self._scheduler.options: + self._scheduler.value = scheduler + + @property + def prepend_text(self): + return self._prepend_text.value + + @prepend_text.setter + def prepend_text(self, prepend_text): + self._prepend_text.value = prepend_text + + @property + def append_text(self): + return self._append_text.value + + @append_text.setter + def append_text(self, append_text): + self._append_text.value = append_text + +class ComputerDropdown(ipw.VBox): + def __init__(self, text='Select computer:', **kwargs): + """ Dropdown for Codes for one input plugin. + + :param text: Text to display before dropdown + :type text: str + """ + + self._dropdown = ipw.Dropdown(options=[], description=text, style={'description_width': 'initial'}, disabled=True) + self._btn_refresh = ipw.Button(description="Refresh", layout=ipw.Layout(width="70px")) + + self._setup_another = ipw.HTML(value="""Setup new computer""", + layout = {'margin': '0px 0px 0px 250px'}) + self._btn_refresh.on_click(self._refresh) + self.output = ipw.Output() + + children = [ipw.HBox([self._dropdown, self._btn_refresh]), + self._setup_another, + self.output] + + super(ComputerDropdown, self).__init__(children=children, **kwargs) + + self._refresh() + + def _get_computers(self): + from aiida.orm.querybuilder import QueryBuilder + current_user = get_automatic_user() + + qb = QueryBuilder() + qb.append( + Computer, filters={'enabled': True}, project=['*'], tag='computer') + + results = qb.all() + + # only computers configured for the current user + results = [r for r in results if r[0].is_user_configured(current_user)] + + self._dropdown.options = {r[0].name:r[0] for r in results} + + def _refresh(self, b=None): + with self.output: + clear_output() + self._get_computers() + if not self.computers: + print("No computers found.") + self._dropdown.disabled = True + else: + self._dropdown.disabled = False + + @property + def computers(self): + return self._dropdown.options + + @property + def selected_computer(self): + try: + return self._dropdown.value + except KeyError: + return None + + @selected_computer.setter + def selected_computer(self, selected_computer): + if selected_computer in self.computers: + self._dropdown.label = selected_computer + diff --git a/aiidalab_widgets_base/search_qb.py b/aiidalab_widgets_base/search_qb.py index 09a208f1e..94c70b57b 100644 --- a/aiidalab_widgets_base/search_qb.py +++ b/aiidalab_widgets_base/search_qb.py @@ -1,8 +1,3 @@ -from aiida import load_dbenv, is_dbenv_loaded -from aiida.backends import settings -if not is_dbenv_loaded(): - load_dbenv(profile=settings.AIIDADB_PROFILE) - from aiida.orm.querybuilder import QueryBuilder from aiida.orm.data.structure import StructureData diff --git a/aiidalab_widgets_base/structures.py b/aiidalab_widgets_base/structures.py index 2dd905687..f327b6a91 100644 --- a/aiidalab_widgets_base/structures.py +++ b/aiidalab_widgets_base/structures.py @@ -1,6 +1,6 @@ from __future__ import print_function - from __future__ import absolute_import + import ase.io import ipywidgets as ipw from fileupload import FileUploadWidget @@ -54,11 +54,6 @@ def __init__(self, text="Upload Structure", node_class=None, **kwargs): self.file_upload.observe(self._on_file_upload, names='data') self.btn_store.on_click(self._on_click_store) - from aiida import load_dbenv, is_dbenv_loaded - from aiida.backends import settings - if not is_dbenv_loaded(): - load_dbenv(profile=settings.AIIDADB_PROFILE) - # pylint: disable=unused-argument def _on_file_upload(self, change): self.tmp_folder = tempfile.mkdtemp() diff --git a/aiidalab_widgets_base/structures_multi.py b/aiidalab_widgets_base/structures_multi.py index 699bd5aa8..e88de5a9e 100644 --- a/aiidalab_widgets_base/structures_multi.py +++ b/aiidalab_widgets_base/structures_multi.py @@ -68,12 +68,6 @@ def __init__(self, text="Upload Zip or Tar archive", node_class=None, **kwargs): self.btn_store_all.on_click(self._on_click_store_all) self.btn_store_selected.on_click(self._on_click_store_selected) - # create aiida-related things - from aiida import load_dbenv, is_dbenv_loaded - from aiida.backends import settings - if not is_dbenv_loaded(): - load_dbenv(profile=settings.AIIDADB_PROFILE) - # function to be called when selection_slider changes def change_structure(self): if self.selection_slider.value is None: diff --git a/cod.ipynb b/cod.ipynb index cf8b2b23e..ae8ec38d1 100644 --- a/cod.ipynb +++ b/cod.ipynb @@ -266,7 +266,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", - "version": "2.7.13" + "version": "2.7.15rc1" } }, "nbformat": 4, diff --git a/codes.ipynb b/codes.ipynb index d009889a0..a9fb7ea0f 100644 --- a/codes.ipynb +++ b/codes.ipynb @@ -40,7 +40,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", - "version": "2.7.15" + "version": "2.7.15rc1" } }, "nbformat": 4, diff --git a/miscellaneous/images/computer.png b/miscellaneous/images/computer.png new file mode 100644 index 0000000000000000000000000000000000000000..9e0465273159c80834ff99dfa53b506acae66dc1 GIT binary patch literal 94771 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 AiiDA Lab widgets + + + """ From acd3b677563b2d17c12e814a2bbe53c1af179bfe Mon Sep 17 00:00:00 2001 From: yakutovicha Date: Sat, 13 Apr 2019 17:06:56 +0200 Subject: [PATCH 2/3] Add aiidalab_display function capable of visualising AiiDA objects in Jupyter (#14) * Visualizers are currently implemented for the following AiiDA objects: 1) ParameterData 2) StructureData 3) CifData 4) FolderData 5) BandsData * Add an example notebook showing the capabilities of `aiidalab_display` function * Add `downloadable` option to `aiidalab_display` function that controls whether the displayed object can downloaded by user. * Make `aiidalab_display` importable from `aiidalab_widgets_base` --- aiida_datatypes.ipynb | 206 +++++++++++++++++++++ aiidalab_widgets_base/__init__.py | 1 + aiidalab_widgets_base/aiida_visualizers.py | 145 +++++++++++++++ aiidalab_widgets_base/display.py | 26 +++ start.py | 1 + 5 files changed, 379 insertions(+) create mode 100644 aiida_datatypes.ipynb create mode 100644 aiidalab_widgets_base/aiida_visualizers.py create mode 100644 aiidalab_widgets_base/display.py diff --git a/aiida_datatypes.ipynb b/aiida_datatypes.ipynb new file mode 100644 index 000000000..2f5a0dcd5 --- /dev/null +++ b/aiida_datatypes.ipynb @@ -0,0 +1,206 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from os import path\n", + "from aiida import load_dbenv, is_dbenv_loaded\n", + "from aiida.backends import settings\n", + "if not is_dbenv_loaded():\n", + " load_dbenv(profile=settings.AIIDADB_PROFILE)\n", + "from aiida.orm import DataFactory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aiidalab_widgets_base import aiidalab_display" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ParameterData" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# visualize ParameterData\n", + "ParameterData = DataFactory('parameter')\n", + "p = ParameterData(dict={\n", + " 'Parameter' :'super long string '*4,\n", + " 'parameter 2' :'value 2',\n", + " 'parameter 3' : 1,\n", + " 'parameter 4' : 2,\n", + "})\n", + "aiidalab_display(p.store(), downloadable=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# create molecule\n", + "from ase.build import molecule\n", + "m = molecule('H2O')\n", + "m.center(vacuum=2.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CifData" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# visualize CifData\n", + "CifData = DataFactory('cif')\n", + "s = CifData(ase=m)\n", + "aiidalab_display(s.store(), downloadable=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## StructureData" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# visualize StructureData\n", + "StructureData = DataFactory('structure')\n", + "s = StructureData(ase=m)\n", + "aiidalab_display(s.store(), downloadable=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## BandsData" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "BandsData = DataFactory('array.bands')\n", + "bs = BandsData()\n", + "kpoints = np.array([[0. , 0. , 0. ], # array shape is 12 * 3\n", + " [0.1 , 0. , 0.1 ],\n", + " [0.2 , 0. , 0.2 ],\n", + " [0.3 , 0. , 0.3 ],\n", + " [0.4 , 0. , 0.4 ],\n", + " [0.5 , 0. , 0.5 ],\n", + " [0.5 , 0. , 0.5 ],\n", + " [0.525 , 0.05 , 0.525 ],\n", + " [0.55 , 0.1 , 0.55 ],\n", + " [0.575 , 0.15 , 0.575 ],\n", + " [0.6 , 0.2 , 0.6 ],\n", + " [0.625 , 0.25 , 0.625 ]])\n", + "\n", + "bands = np.array([\n", + " [-5.64024889, 6.66929678, 6.66929678, 6.66929678, 8.91047649], # array shape is 12 * 5, where 12 is the size of the kpoints mesh\n", + " [-5.46976726, 5.76113772, 5.97844699, 5.97844699, 8.48186734], # and 5 is the number of states\n", + " [-4.93870761, 4.06179965, 4.97235487, 4.97235488, 7.68276008],\n", + " [-4.05318686, 2.21579935, 4.18048674, 4.18048675, 7.04145185],\n", + " [-2.83974972, 0.37738276, 3.69024464, 3.69024465, 6.75053465],\n", + " [-1.34041116, -1.34041115, 3.52500177, 3.52500178, 6.92381041],\n", + " [-1.34041116, -1.34041115, 3.52500177, 3.52500178, 6.92381041],\n", + " [-1.34599146, -1.31663872, 3.34867603, 3.54390139, 6.93928289],\n", + " [-1.36769345, -1.24523403, 2.94149041, 3.6004033 , 6.98809593],\n", + " [-1.42050683, -1.12604118, 2.48497007, 3.69389815, 7.07537154],\n", + " [-1.52788845, -0.95900776, 2.09104321, 3.82330632, 7.20537566],\n", + " [-1.71354964, -0.74425095, 1.82242466, 3.98697455, 7.37979746]])\n", + "bs.set_kpoints(kpoints)\n", + "bs.set_bands(bands)\n", + "labels = [(0, u'GAMMA'),\n", + " (5, u'X'),\n", + " (6, u'Z'),\n", + " (11, u'U')]\n", + "bs.labels = labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "aiidalab_display(bs.store()) # to visualize the bands" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## FolderData" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "FolderData = DataFactory('folder')\n", + "fd = FolderData()\n", + "with fd.folder.open(path.join('path','test1.txt'), 'w') as fobj:\n", + " fobj.write('content of test1 file')\n", + "with fd.folder.open(path.join('path','test2.txt'), 'w') as fobj:\n", + " fobj.write('content of test2\\nfile')\n", + "with fd.folder.open(path.join('path','test_long.txt'), 'w') as fobj:\n", + " fobj.write('content of test_long file'*1000)\n", + "aiidalab_display(fd.store(), downloadable=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.15rc1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index e06ce9d2f..7239580fb 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -9,5 +9,6 @@ from .codes import CodeDropdown, AiiDACodeSetup, extract_aiidacodesetup_arguments # noqa from .computers import SshComputerSetup, extract_sshcomputersetup_arguments # noqa from .computers import AiidaComputerSetup, extract_aiidacomputer_arguments # noqa +from .display import aiidalab_display # noqa __version__ = "0.2.0a1" diff --git a/aiidalab_widgets_base/aiida_visualizers.py b/aiidalab_widgets_base/aiida_visualizers.py new file mode 100644 index 000000000..8283e52c7 --- /dev/null +++ b/aiidalab_widgets_base/aiida_visualizers.py @@ -0,0 +1,145 @@ +from __future__ import print_function +import os + +import ipywidgets as ipw + +class ParameterDataVisualizer(ipw.HTML): + """Visualizer class for ParameterData object""" + def __init__(self, parameter, downloadable=True, **kwargs): + super(ParameterDataVisualizer, self).__init__(**kwargs) + import pandas as pd + # Here we are defining properties of 'df' class (specified while exporting pandas table into html). + # Since the exported object is nothing more than HTML table, all 'standard' HTML table settings + # can be applied to it as well. + # For more information on how to controle the table appearance please visit: + # https://css-tricks.com/complete-guide-table-element/ + self.value = ''' + + ''' + pd.set_option('max_colwidth', 40) + df = pd.DataFrame([(key, value) for key, value + in sorted(parameter.get_dict().items()) + ], columns=['Key', 'Value']) + self.value += df.to_html(classes='df', index=False) # specify that exported table belongs to 'df' class + # this is used to setup table's appearance using CSS + if downloadable: + import base64 + payload = base64.b64encode(df.to_csv(index=False).encode()).decode() + fname = '{}.csv'.format(parameter.pk) + to_add = """Download table in csv format: {title}""" + self.value += to_add.format(filename=fname, payload=payload,title=fname) + +class StructureDataVisualizer(ipw.VBox): + """Visualizer class for StructureData object""" + def __init__(self, structure, downloadable=True, **kwargs): + import nglview + self._structure = structure + viewer = nglview.NGLWidget() + viewer.add_component(nglview.ASEStructure(self._structure.get_ase())) # adds ball+stick + viewer.add_unitcell() + children = [viewer] + if downloadable: + self.file_format = ipw.Dropdown( + options=['xyz', 'cif'], + description="File format:", + ) + self.download_btn = ipw.Button(description="Download") + self.download_btn.on_click(self.download) + children.append(ipw.HBox([self.file_format, self.download_btn])) + super(StructureDataVisualizer, self).__init__(children, **kwargs) + + def download(self, b=None): + import base64 + from tempfile import TemporaryFile + from IPython.display import Javascript + with TemporaryFile() as fobj: + self._structure.get_ase().write(fobj, format=self.file_format.value) + fobj.seek(0) + b64 = base64.b64encode(fobj.read()) + payload = b64.decode() + js = Javascript( + """ + var link = document.createElement('a'); + link.href = "data:;base64,{payload}" + link.download = "{filename}" + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + """.format(payload=payload,filename=str(self._structure.id)+'.'+self.file_format.value) + ) + display(js) + +class FolderDataVisualizer(ipw.VBox): + """Visualizer class for FolderData object""" + def __init__(self, folder, downloadable=True, **kwargs): + self._folder = folder + self.files = ipw.Dropdown( + options=self._folder.get_folder_list(), + description="Select file:", + ) + self.text = ipw.Textarea( + value="", + description='File content:', + layout={'width':"900px", 'height':'300px'}, + disabled=False + ) + self.change_file_view() + self.files.observe(self.change_file_view, names='value') + children = [self.files, self.text] + if downloadable: + self.download_btn = ipw.Button(description="Download") + self.download_btn.on_click(self.download) + children.append(self.download_btn) + super(FolderDataVisualizer, self).__init__(children, **kwargs) + + def change_file_view(self, b=None): + with open(self._folder.get_abs_path(self.files.value), "rb") as fobj: + self.text.value = fobj.read() + + def download(self, b=None): + import base64 + from IPython.display import Javascript + with open(self._folder.get_abs_path(self.files.value), "rb") as fobj: + b64 = base64.b64encode(fobj.read()) + payload = b64.decode() + js = Javascript( + """ + var link = document.createElement('a'); + link.href = "data:;base64,{payload}" + link.download = "{filename}" + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + """.format(payload=payload,filename=self.files.value) + ) + display(js) + +class BandsDataVisualizer(ipw.VBox): + """Visualizer class for BandsData object""" + def __init__(self, bands, **kwargs): + from bokeh.plotting import figure + from bokeh.io import show, output_notebook + output_notebook(hide_banner=True) + out = ipw.Output() + with out: + plot_info = bands._get_bandplot_data(cartesian=True, join_symbol="|") + y = plot_info['y'].transpose().tolist() + x = [plot_info['x'] for i in range(len(y))] + labels = plot_info['labels'] + p = figure(y_axis_label='Dispersion ({})'.format(bands.units)) + p.multi_line(x, y, line_width=2) + p.xaxis.ticker = [l[0] for l in labels] + p.xaxis.major_label_overrides = {int(l[0]) if l[0].is_integer() else l[0]:l[1] for l in labels} + # int(l[0]) if l[0].is_integer() else l[0] + # This trick was suggested here: https://github.com/bokeh/bokeh/issues/8166#issuecomment-426124290 + show(p) + children = [out] + super(BandsDataVisualizer, self).__init__(children, **kwargs) \ No newline at end of file diff --git a/aiidalab_widgets_base/display.py b/aiidalab_widgets_base/display.py new file mode 100644 index 000000000..ce4f0489d --- /dev/null +++ b/aiidalab_widgets_base/display.py @@ -0,0 +1,26 @@ +from __future__ import print_function + +import importlib +from IPython.display import display + +AIIDA_VISUALIZER_MAPPING = { + 'data.parameter.ParameterData.' : 'ParameterDataVisualizer', + 'data.structure.StructureData.' : 'StructureDataVisualizer', + 'data.cif.CifData.' : 'StructureDataVisualizer', + 'data.folder.FolderData.' : 'FolderDataVisualizer', + 'data.array.bands.BandsData.' : 'BandsDataVisualizer', +} + +def aiidalab_display(obj, downloadable=True, **kwargs): + """Display AiiDA data types in Jupyter notebooks. + + :param downloadable: If True, add link/button to download content of displayed AiiDA object. + + Defers to IPython.display.display for any objects it does not recognize. + """ + from aiidalab_widgets_base import aiida_visualizers + try: + visualizer = getattr(aiida_visualizers, AIIDA_VISUALIZER_MAPPING[obj.type]) + display(visualizer(obj, downloadable=downloadable), **kwargs) + except KeyError: + display(obj, **kwargs) \ No newline at end of file diff --git a/start.py b/start.py index a8f5dd359..007a5b389 100644 --- a/start.py +++ b/start.py @@ -16,6 +16,7 @@ From 76974ca55e1353c870318e1cbb9fd9d441fa8802 Mon Sep 17 00:00:00 2001 From: Aliaksandr Yakutovich Date: Sat, 13 Apr 2019 20:10:39 +0200 Subject: [PATCH 3/3] prepare v0.3.0b1 release --- aiidalab_widgets_base/__init__.py | 2 +- metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index 7239580fb..b8d09f6c3 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -11,4 +11,4 @@ from .computers import AiidaComputerSetup, extract_aiidacomputer_arguments # noqa from .display import aiidalab_display # noqa -__version__ = "0.2.0a1" +__version__ = "0.3.0b1" diff --git a/metadata.json b/metadata.json index be3b3e17d..e7544ce63 100644 --- a/metadata.json +++ b/metadata.json @@ -2,7 +2,7 @@ "title": "AiiDA Lab Widgets", "description": "Reusable widgets for applications in the AiiDA Lab.", "authors": "AiiDA Team", - "version": "0.2.0a2", + "version": "0.3.0b1", "logo": "miscellaneous/logos/aiidalab.png", "state": "stable" }