""""Basic tools and mixin classes for AbiPy panels."""
import io
import tempfile
import numpy as np
import param
import time
import shutil
import panel as pn
import panel.widgets as pnw
import bokeh.models.widgets as bkw
import pandas as pd
from monty.functools import lazy_property
from monty.termcolor import cprint
from abipy.core import abinit_units as abu
from abipy.core.structure import Structure
from abipy.tools.plotting import push_to_chart_studio
_ABINIT_TEMPLATE_NAME = "FastList"
[docs]def get_abinit_template_cls_kwds():
cls = get_template_cls_from_name(_ABINIT_TEMPLATE_NAME)
kwds = dict(header_background="#ff8c00", # Dark orange
#favicon="assets/img/abinit_favicon.ico",
#logo="assets/img/abipy_logo.png", # TODO: Need new panel version to fix logo alignment in FastLIst.
)
return cls, kwds
[docs]def set_abinit_template(template_name):
global _ABINIT_TEMPLATE_NAME
_ABINIT_TEMPLATE_NAME = template_name
[docs]def abipanel(panel_template="FastList"):
"""
Activate panel extensions used by AbiPy. Return panel module.
Args:
panel_template: String with the name of the panel template to be used by default.
"""
try:
import panel as pn
except ImportError as exc:
cprint("Use `conda install panel` or `pip install panel` to install the python package.", "red")
raise exc
set_abinit_template(panel_template)
extensions = [
"plotly",
"mathjax",
#"katex",
"ace",
]
#pn.extension(loading_spinner='petal', loading_color='#00aa41')
#print("loading extensions:", extensions)
pn.extension(*extensions) # , raw_css=[css])
pn.config.js_files.update({
# This for copy to clipboard.
"clipboard": "https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js",
# This for jsmol.
"jsmol": "http://chemapps.stolaf.edu/jmol/jsmol/JSmol.min.js",
})
#pn.config.js_files.update({
# 'ngl': 'https://cdn.jsdelivr.net/gh/arose/ngl@v2.0.0-dev.33/dist/ngl.js',
#})
#pn.extension(comms='ipywidgets')
#pn.config.sizing_mode = "stretch_width"
return pn
[docs]def gen_id(n=1, pre="uuid-"):
"""
Generate ``n`` universally unique identifiers prepended with ``pre`` string.
Return string if n == 1 or list of strings if n > 1
"""
# The HTML4 spec says:
# ID and NAME tokens must begin with a letter ([A-Za-z]) and may be followed by any number of letters,
# digits ([0-9]), hyphens ("-"), underscores ("_"), colons (":"), and periods (".").
import uuid
if n == 1:
return pre + str(uuid.uuid4())
elif n > 1:
return [pre + str(uuid.uuid4()) for i in range(n)]
else:
raise ValueError("n must be > 0 but got %s" % str(n))
[docs]def get_template_cls_from_name(name):
"""
Return panel template from string.
Support name in the form `FastList` as well as `FastListTemplate`.
"""
# Example: pn.template.FastGridTemplate or pn.template.GoldenTemplate
if hasattr(pn.template, name):
return getattr(pn.template, name)
try_name = name + "Template"
if hasattr(pn.template, try_name):
return getattr(pn.template, try_name)
raise ValueError(f"""
Don't know how to return panel template from string: {name}
Possible templates are: {list(pn.template.__dict__.keys())}
""")
[docs]def depends_on_btn_click(btn_name, show_exc=True):
"""
This decorator is used for callbacks triggered by a button of name `btn_name`
If show_exc is True, a Markdown pane with the backtrace is returned
if an exception is raised.
"""
def decorator(func):
from functools import wraps
@wraps(func)
def decorated(*args, **kwargs):
self = args[0]
btn = getattr(self, btn_name)
if btn.clicks == 0: return
with ButtonContext(btn):
return func(*args, **kwargs)
f = pn.depends(f"{btn_name}.clicks")(decorated)
if show_exc: f = show_exception(f)
return f
return decorator
[docs]def my_depends(*args, **kwargs):
show_exc = kwargs.pop("show_exc", False)
def decorator(func):
from functools import wraps
@wraps(func)
def decorated(*args, **kwargs):
f = pn.depends(func, *args, **kwargs)
if show_exc: f = show_exception(f)
return f
return decorator
[docs]def show_exception(func):
"""
This decorator returns a Markdown pane with the backtrace
if the function raises an exception.
"""
from functools import wraps
@wraps(func)
def decorated(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as exc:
print(exc)
import traceback
return pn.Column(pn.pane.Markdown("```shell\n%s\n```" % traceback.format_exc()),
sizing_mode="stretch_width")
return decorated
[docs]class HTMLwithClipboardBtn(pn.pane.HTML):
"""
Receives an HTML string and returns an HTML pane with a button that allows the user
to copy the content to the system clipboard.
Requires call to abipanel to load the JS extension.
"""
# This counter is shared by all the instances. We use it so that the js script is included only once.
_init_counter = [0]
def __init__(self, object=None, btn_cls=None, **params):
super().__init__(object=object, **params)
self._init_counter[0] += 1
my_id = gen_id()
btn_cls = "bk bk-btn bk-btn-default" if btn_cls is None else str(btn_cls)
# Build new HTML string with js section if first call.
new_text = f"""
<div id="{my_id}">{self.object}</div>
<br>
<button class="clip-btn {btn_cls}" type="button" data-clipboard-target="#{my_id}"> Copy to clipboard </button>
<hr>
"""
if self._init_counter[0] == 1:
new_text += "<script> $(document).ready(function() {new ClipboardJS('.clip-btn')}) </script> "
self.object = new_text
# https://github.com/MarcSkovMadsen/awesome-panel/blob/master/application/pages/js_actions/js_actions.py
#def copy_to_clipboard():
# """Copy"""
# source_textarea = pn.widgets.TextAreaInput(
# value="Copy this text to the clipboard by clicking the button",
# height=100,
# )
# copy_source_button = pn.widgets.Button(name="✂ Copy Source Value", button_type="primary")
# copy_source_code = "navigator.clipboard.writeText(source.value);"
# copy_source_button.js_on_click(args={"source": source_textarea}, code=copy_source_code)
# paste_text_area = pn.widgets.TextAreaInput(placeholder="Paste your value here", height=100)
# return pn.Column(
# pn.Row(source_textarea, copy_source_button, paste_text_area),
# name="✂ Copy to Clipboard",
# )
[docs]def mpl(fig, sizing_mode='stretch_width', with_controls=False, with_divider=True, **kwargs):
"""
Helper function returning a panel Column with a matplotly pane followed by
a divider and (optionally) controls to customize the figure.
"""
col = pn.Column(sizing_mode=sizing_mode); ca = col.append
mpl_pane = pn.pane.Matplotlib(fig, **kwargs)
ca(mpl_pane)
if with_controls:
ca(pn.Accordion(("matplotlib controls", mpl_pane.controls(jslink=True))))
ca(pn.layout.Divider())
if with_divider:
ca(pn.layout.Divider())
return col
[docs]def ply(fig, sizing_mode='stretch_width', with_chart_studio=True, with_help=True,
with_divider=True, with_controls=False):
"""
Helper function returning a panel Column with a plotly pane, buttons to push the figure
to plotly chart studio and, optionally, controls to customize the figure.
"""
col = pn.Column(sizing_mode=sizing_mode); ca = col.append
plotly_pane = pn.pane.Plotly(fig, config={'responsive': True})
ca(plotly_pane)
if with_chart_studio:
md = pn.pane.Markdown("""
The button on the left allows you to **upload the figure** to the
plotly [chart studio server](https://plotly.com/chart-studio-help/)
so that it is possible to share the figure or customize it via the chart studio editor.
In order to use this feature, you need to create a free account following the instructions
reported [in this page](https://plotly.com/chart-studio-help/how-to-sign-up-to-plotly/).
Then **add the following section** to your $HOME/.pmgrc.yaml configuration file:
```yaml
PLOTLY_USERNAME: john_doe # Replace with your username
PLOTLY_API_KEY: secret # To get your api_key go to: profile > settings > regenerate key
```
so that AbiPy can authenticate your user on the chart studio portal before pushing the figure to the cloud.
If everything is properly configured, a new window is automatically created in your browser.
""")
btn = pnw.Button(name="Upload to chart studio server")
def push_to_cs(event):
with ButtonContext(btn):
push_to_chart_studio(fig)
btn.on_click(push_to_cs)
if with_help:
acc = pn.Accordion(("What is this?", md))
ca(pn.Row(btn, acc))
else:
ca(pn.Row(btn))
if with_controls:
ca(pn.Accordion(("plotly controls", plotly_pane.controls(jslink=True))))
if with_divider:
ca(pn.layout.Divider())
return col
[docs]def dfc(df,
wdg_type="dataframe",
#wdg_type="tabulator", # More recent version
with_export_btn=True, with_controls=False, with_divider=True, transpose=False, **kwargs):
"""
Helper function returning a panel Column with a DataFrame or Tabulator widget followed by
a divider and (optionally) controls to customize the figure.
Note that not all the options work as exected. See comments below.
"""
if "disabled" not in kwargs: kwargs["disabled"] = True
#if "sizing_mode" not in kwargs: kwargs["sizing_mode"] = "stretch_both"
if "sizing_mode" not in kwargs: kwargs["sizing_mode"] = "scale_width"
if transpose:
df = df.transpose()
if wdg_type == "dataframe":
if "auto_edit" not in kwargs: kwargs["auto_edit"] = False
w = pnw.DataFrame(df, **kwargs)
elif wdg_type == "tabulator":
w = pnw.Tabulator(df, **kwargs)
else:
raise ValueError(f"Don't know how to handle widget type: `{wdg_type}`")
col = pn.Column(sizing_mode='stretch_both'); ca = col.append
ca(w)
if with_export_btn:
# Define callbacks with closure.
#clip_button = pnw.Button(name="Copy to clipboard")
#def to_clipboard(event):
# df.to_clipboard()
#clip_button.on_click(to_clipboard)
def to_xlsx():
"""
Based on https://panel.holoviz.org/gallery/simple/file_download_examples.html
NB: This requires xlsxwriter package else pandas raises ModuleNotFoundError.
"""
output = io.BytesIO()
writer = pd.ExcelWriter(output, engine='xlsxwriter')
df.to_excel(writer, sheet_name="DataFrame")
writer.save() # Important!
output.seek(0)
return output
def to_latex():
"""Convert DataFrame to latex string."""
output = io.StringIO()
df.to_latex(buf=output)
output.seek(0)
return output
def to_md():
"""Convert DataFrame to markdown string."""
output = io.StringIO()
df.to_markdown(buf=output)
output.seek(0)
return output
def to_json():
"""Convert DataFrame to json string."""
output = io.StringIO()
df.to_json(path_or_buf=output)
output.seek(0)
return output
d = dict(
xlsx=pnw.FileDownload(filename="data.xlsx", callback=to_xlsx),
tex=pnw.FileDownload(filename="data.tex", callback=to_latex),
md=pnw.FileDownload(filename="data.md", callback=to_md),
json=pnw.FileDownload(filename="data.json", callback=to_json),
)
def download(event):
#print(f'Clicked menu item: "{event.new}"')
file_download = d[event.new]
print(file_download)
#file_download._clicks = -1
#print("Calling transfer")
file_download._transfer()
#return file_download.callback()
# FIXME: Menu button occupies less space but the upload does not work
#menu_btn = pnw.MenuButton(name='Export to:', items=list(d.keys()))
#menu_btn.on_click(download)
#ca(menu_btn)
# For the time being we use a Row with buttons.
ca(pn.Row(*d.values(), sizing_mode="scale_width"))
if with_controls:
ca(pn.Accordion(("dataframe controls", w.controls(jslink=True))))
if with_divider:
ca(pn.layout.Divider())
return col
[docs]class MyMarkdown(pn.pane.Markdown):
"""
A Markdown pane renders the markdown markup language to HTML and
displays it inside a bokeh Div model. It has no explicit
priority since it cannot be easily be distinguished from a
standard string, therefore it has to be invoked explicitly.
"""
extensions = param.List(default=[
# Extensions used by the superclass.
"extra", "smarty", "codehilite",
# My extensions
#'pymdownx.arithmatex',
#'pymdownx.details',
#"pymdownx.tabbed",
],
doc="""Markdown extension to apply when transforming markup."""
)
[docs]class ButtonContext():
"""
A context manager for buttons triggering computations on the server.
This manager disables the button when we __enter__ and changes the name of the button to "running".
It reverts to the initial state of the button ocne __exit__ is invoked, showing the Exception type
in a "red" button if an exception is raised during the computation.
This a very important tool because we need to disable the button when we start the computation
to prevent the user from triggering multiple callbacks while the server is still working.
At the same time, whathever happens in the callback, the button should go back to "clickable" mode
when the callback returns so that the user can try to change the parameters and rerun.
Note also that we want to provide some graphical feedback to the user if something goes wrong.
At present we don't expose the python traceback on the client.
It would be nice but we need panel machinery to do that.
Moreover this is not the recommended approach for security reasons so we just change the "color"
of the button and use the string representation of the exception as button name.
"""
def __init__(self, btn):
self.btn = btn
self.prev_name, self.prev_type = btn.name, btn.button_type
def __enter__(self):
# Disable the button.
self.btn.name = "Executing ..."
self.btn.button_type = "warning"
self.btn.disabled = True
return self.btn
def __exit__(self, exc_type, exc_value, traceback):
# First of all, reenable the button so that the user can stil interact with the GUI.
self.btn.disabled = False
if exc_type:
# Exception --> signal to the user that something went wrong for 2 seconds.
self.btn.name = str(exc_type)
self.btn.button_type = "danger"
time.sleep(2)
# Back to the original button state.
self.btn.name, self.btn.button_type = self.prev_name, self.prev_type
# Don't handle the exception
return None
[docs]class Loading():
"""
A context manager for setting the loading attribute of a panel object.
"""
def __init__(self, panel_obj, err_wdg=None, width=70):
self.panel_obj = panel_obj
self.err_wdg = err_wdg
if err_wdg is not None:
self.err_wdg.object = ""
self.width = int(width)
def __enter__(self):
self.panel_obj.loading = True
return self.panel_obj
def __exit__(self, exc_type, exc_value, traceback):
self.panel_obj.loading = False
if self.err_wdg is not None:
if exc_type:
from textwrap import fill
self.err_wdg.object = "```sh\n%s\n```" % fill(str(exc_value), width=self.width)
#else:
# self.err_wdg.object = "OK"
# Don't handle the exception
return None
[docs]class ActiveBar():
"""
A context manager that sets progress.active to True on entry and False when we exit.
"""
def __init__(self, progress, err_wdg=None, width=70):
self.progress = progress
self.err_wdg = err_wdg
if err_wdg is not None:
self.err_wdg.object = ""
self.width = int(width)
def __enter__(self):
self.progress.active = True
return self.progress
def __exit__(self, exc_type, exc_value, traceback):
self.progress.active = False
if exc_type:
# Change the color to signal the user that something bad happened.
self.progress.bar_color = "danger"
if self.err_wdg is not None:
from textwrap import fill
self.err_wdg.object = "```sh\n%s\n```" % fill(str(exc_value), width=self.width)
# Don't handle the exception
return None
[docs]class AbipyParameterized(param.Parameterized):
"""
Base class for AbiPy panels. Provides helper functions for typical operations needed for
building dashboard and basic parameters supported by the subclasses.
"""
verbose = param.Integer(0, bounds=(0, None), doc="Verbosity Level")
mpi_procs = param.Integer(1, bounds=(1, None), doc="Number of MPI processes used for running Fortran code.")
# This flag is set to True if we are serving apps from the Abinit server.
# It is used to impose limitations on what users can do and select the options that should be exposed.
# For instance, structure_viewer == "Vesta" does not make sense in we are not serving from a local server.
#
has_remote_server = param.Boolean(False)
#has_remote_server = param.Boolean(True)
warning = pn.pane.Markdown(
"""
Widgets are **shared by the different tabs**.
This means that if you change the value of one of these variables in the active tab,
the same value will **automagically** appear in the other tabs yet results/figures
are not automatically recomputed when you change the value.
In other words, if you change some variable in the active tab and then you move to another tab,
the results/figures (if any) are stil computed with the **old input** hence you will have to
recompute the new results by clicking the button.
""", name="warning")
def __init__(self, **params):
super().__init__(**params)
if self.has_remote_server:
self.param.mpi_procs.bounds = (1, 1)
print("Changing mpi_procs.bounds")
print("self.param.mpi_procs:", self.param.mpi_procs.bounds)
[docs] @lazy_property
def mpl_kwargs(self):
"""Default arguments passed to AbiPy matplotlib plot methods."""
return dict(show=False, fig_close=True)
[docs] def pws_col(self, keys):
return pn.Column(*self.pws(keys))
#def pws_row(self, keys):
# return pn.Row(*self.pws(keys))
[docs] def pws(self, keys):
"""
Helper function returning the list of parameters and widgets defined in self from a list of strings.
Accepts also widget or parameter instances.
"""
items, miss = [], []
for k in keys:
if isinstance(k, str):
if k in self.param:
items.append(self.param[k])
elif hasattr(self, k):
items.append(getattr(self, k))
elif k.startswith("#"):
# Markdown string
items.append(k)
else:
miss.append(k)
else:
# Assume widget instance.
items.append(k)
if miss:
raise ValueError(f"Cannot find `{str(miss)}` in param or in attribute space")
#for item in items:
# print("item", item, "of type:", type(item))
return items
[docs] def helpc(self, method_name, extra_items=None):
"""
Add accordion with a brief description and a warning after the button.
The description of the tool is taken from the docstring of the callback.
Return Column.
"""
col = pn.Column(); ca = col.append
acc = pn.Accordion(("Help", pn.pane.Markdown(getattr(self, method_name).__doc__)))
if hasattr(self, "warning"):
acc.append(("Warning", self.warning))
if extra_items is not None:
for name, attr in extra_items:
acc.append((name, item))
ca(pn.layout.Divider())
ca(acc)
return col
[docs] def wdg_exts_with_get_panel(self, name='File extensions supported:'):
"""
Return Select widget with the list of file extensions implementing a get_panel method.
"""
from abipy.abilab import extcls_supporting_panel
exts = [e[0] for e in extcls_supporting_panel(as_table=False)]
return pn.widgets.Select(name=name, options=exts)
[docs] @staticmethod
def html_with_clipboard_btn(html_str, **kwargs):
return HTMLwithClipboardBtn(html_str, **kwargs)
[docs] @staticmethod
def get_software_stack():
"""Return column with version of python packages in tabular format."""
from abipy.abilab import software_stack
return pn.Column("## Software stack:", dfc(software_stack(as_dataframe=True), with_export_btn=False),
sizing_mode="scale_width")
[docs] @staticmethod
def get_alert_data_transfer():
return pn.pane.Alert("""
Please note that this web interface is not designed to handle **large data transfer**.
To post-process the data stored in a big file e.g. a WFK.nc file,
we strongly suggest executing the **abigui.py** script on the same machine where the file is hosted.
Also, note that examples of post-processing scripts are available in the
[AbiPy gallery](https://abinit.github.io/abipy/gallery/index.html).
Last but not least, keep in mind that **the file extension matters** when uploading a file
so don't change the default extension used by ABINIT.
Also, use `.abi` for ABINIT input files and `.abo` for the main output file.
""", alert_type="info")
[docs] @staticmethod
def get_template_cls_from_name(template):
return get_template_cls_from_name(template)
[docs] def get_abinit_template_cls_kwds(self):
return get_abinit_template_cls_kwds()
[docs] def get_template_from_tabs(self, tabs, template, tabs_location="above", closable=False):
"""
This method receives panel Tabs or a dictionary,
include them in a template and return the template.
"""
if isinstance(tabs, dict):
tabs = pn.Tabs(*tabs.items(), tabs_location=tabs_location, closable=closable,
sizing_mode="stretch_width")
if template is None:
return tabs
cls = get_template_cls_from_name(template)
kwargs = dict(
# A title to show in the header. Also added to the document head meta settings and as the browser tab title.
title=self.__class__.__name__,
header_background="#ff8c00", # Dark orange
#favicon (str): URI of favicon to add to the document head (if local file, favicon is base64 encoded as URI).
#favicon="assets/img/abinit_favicon.ico",
#logo="assets/img/abipy_logo.png", # TODO: Need new panel version to fix logo alignment in FastLIst.
#sidebar_footer (str): Can be used to insert additional HTML. For example a menu, some additional info, links etc.
#enable_theme_toggle=False, # If True a switch to toggle the Theme is shown. Default is True.
)
template = cls(**kwargs)
if hasattr(template.main, "append"):
template.main.append(tabs)
else:
# Assume main area acts like a GridSpec
template.main[:,:] = tabs
# Get widgets associated to Ph-bands tab and insert them in the sidebar.
#row = tabs[0]
#controllers, out = row[0], row[1]
#template.sidebar.append(controllers)
#template.main.append(out)
return template
[docs]class PanelWithStructure(AbipyParameterized):
"""
Mixin class for panel objects providing a |Structure| object.
"""
structure_viewer = param.ObjectSelector(default="jsmol",
objects=["jsmol", "vesta", "xcrysden", "vtk", "crystalk", "ngl",
"matplotlib", "plotly", "ase_atoms", "mayavi"])
def __init__(self, structure, **params):
super().__init__(**params)
self.structure = structure
if self.has_remote_server:
# Change the list of allowed visulizers if remote server.
self.param.structure_viewer.objects = ["jsmol", "crystalk", "ngl", "matplotlib", "plotly", "ase_atoms"]
self.view_structure_btn = pnw.Button(name="View structure", button_type='primary')
[docs] @depends_on_btn_click('view_structure_btn')
def on_view_structure(self):
"""Visualize input structure."""
v = self.structure_viewer
if v == "jsmol":
return jsmol_html(self.structure)
#pn.extension(comms='ipywidgets') #, js_files=js_files)
#view = self.structure.get_jsmol_view()
#from ipywidgets_bokeh import IPyWidget
#view = IPyWidget(widget=view) #, width=800, height=300)
#import ipywidgets as ipw
#from IPython.display import display
#display(view)
#return pn.Row(display(view))
#view = pn.ipywidget(view)
view = pn.panel(view)
#view = pn.pane.IPyWidget(view)
print(view)
#view = pn.Column(view, sizing_mode='stretch_width')
return view
if v == "crystalk":
view = self.structure.get_crystaltk_view()
return pn.panel(view)
if v == "plotly":
return ply(self.structure.plotly(show=False))
if v == "ngl":
from pymatgen.io.babel import BabelMolAdaptor
from pymatgen.io.xyz import XYZ
# string_data = self.structure.to(fmt="xyz")
#writer = BabelMolAdaptor(self)
#string_data = str(XYZ(self.structure))
#adapt = BabelMolAdaptor.from_string(string_data, file_format="xyz")
##pdb_string =
#print(pdb_string)
#from awesome_panel_extesions.pane.widgets.ngl_viewer import NGLViewer
#view = NGLViewer()
view.pdb_string = pdb_string
return view
#js_files = {'ngl': 'https://cdn.jsdelivr.net/gh/arose/ngl@v2.0.0-dev.33/dist/ngl.js'}
#pn.extension(comms='ipywidgets', js_files=js_files)
#view = self.structure.get_ngl_view()
#return pn.panel(view)
#pn.config.js_files["ngl"]="https://cdn.jsdelivr.net/gh/arose/ngl@v2.0.0-dev.33/dist/ngl.js"
#pn.extension()
html = """<div id="viewport" style="width:100%; height:100%;"></div>
<script>
stage = new NGL.Stage("viewport");
stage.loadFile("rcsb://1NKT.mmtf", {defaultRepresentation: true});
</script>"""
ngl_pane = pn.pane.HTML(html, height=500, width=500)
return pn.Row(ngl_pane)
view = self.structure.get_ngl_view()
#return self.structure.crystaltoolkitview()
#import nglview as nv
#view = nv.demo(gui=False)
if v == "ase_atoms":
return mpl(self.structure.plot_atoms(rotations="default", **self.mpl_kwargs))
return self.structure.visualize(appname=self.structure_viewer)
[docs] def get_struct_view_tab_entry(self):
"""
Return tab entry to visualize the structure.
"""
return pn.Row(
self.pws_col(["### Visualize structure", "structure_viewer", "view_structure_btn",
self.helpc("on_view_structure")]),
pn.Column(self.on_view_structure, self.get_structure_info())
)
[docs] def get_structure_info(self):
"""
Return Column with lattice parameters, angles and atomic positions grouped by type.
"""
return get_structure_info(self.structure)
[docs]def get_structure_info(structure):
"""
Return Column with lattice parameters, angles and atomic positions grouped by type.
"""
col = pn.Column(sizing_mode='scale_width'); ca = col.append; cext = col.extend
d = structure.get_dict4pandas(with_spglib=True)
keys = index = [#"formula", "natom", "volume",
"abi_spg_number",
"spglib_symb", "spglib_num", "spglib_lattice_type"]
df_spg = pd.Series(data=d, index=index).to_frame()
cext(["# Spacegroup:", dfc(df_spg, with_export_btn=False)])
# Build dataframe with lattice lenghts.
rows = []; keys = ("a", "b", "c")
rows.append({k: d[k] * abu.Ang_Bohr for k in keys})
rows.append({k: d[k] for k in keys})
df_len = pd.DataFrame(rows, index=["Å", "Bohr"]).transpose().rename_axis("Lattice lenghts")
# Build dataframe with lattice angles.
rows = []; keys = ("alpha", "beta", "gamma")
rows.append({k: d[k] for k in keys})
rows.append({k: np.radians(d[k]) for k in keys})
df_ang = pd.DataFrame(rows, index=["Degrees", "Radians"]).transpose().rename_axis("Lattice angles")
cext(["# Lattice lengths:", dfc(df_len, with_export_btn=False)])
cext(["# Lattice angles:", dfc(df_ang, with_export_btn=False)])
#row = pn.Row(dfc(df_len, with_export_btn=False), dfc(df_ang, with_export_btn=False), sizing_mode="scale_width")
#ca(row)
# Build dataframe with atomic positions grouped by element symbol.
symb2df = structure.get_symb2coords_dataframe()
accord = pn.Accordion(sizing_mode='stretch_width')
for symb, df in symb2df.items():
accord.append((f"Coordinates of {symb} sites:", dfc(df, with_export_btn=False)))
ca(accord)
return col
[docs]class NcFileMixin(param.Parameterized):
"""
This mixin class allows the user to inspect the dimensions and the variables
reported in a netcdf file. Subclasses should implement the `ncfile` property
"""
#def __init__(self, ncfile, **params)
# super().__init__(**params)
# self.ncfile = ncfile
@property
def ncfile(self):
"""abc does not play well with parametrized so we rely on this to enforce the protocol."""
raise NotImplementedError("subclass should implement the `ncfile` property.")
[docs] def get_ncfile_panel(self):
col = pn.Column(sizing_mode='stretch_width'); ca = col.append
#nc_grpname = pnw.Select(name="nc group name", options=["/"])
# Get dataframe with dimensions.
dims_df = self.ncfile.get_dims_dataframe(path="/")
ca(dfc(dims_df))
#vars_df = self.ncfile.get_dims_dataframe(path="/")
#ca(dfc(vars_df))
#ca(("NCFile", pn.Row(
# pn.Column("# NC dimensions and variables",
# dfc(dims_df, wdg_type="tabulator"),
# )),
#))
return col
[docs]class PanelWithElectronBands(PanelWithStructure):
"""
Mixin class for panel object associated to AbiPy object providing an |ElectronBands| object.
Subclasses should implement the `ebands` property.
"""
# Bands plot
with_gaps = param.Boolean(False)
#ebands_ylims
#ebands_e0
# e0: Option used to define the zero of energy in the band structure plot. Possible values:
# - `fermie`: shift all eigenvalues to have zero energy at the Fermi energy (`self.fermie`).
# - Number e.g e0=0.5: shift all eigenvalues to have zero energy at 0.5 eV
# - None: Don't shift energies, equivalent to e0=0
set_fermie_to_vbm = param.Boolean(False, doc="Set Fermie to VBM")
# e-DOS plot.
edos_method = param.ObjectSelector(default="gaussian", objects=["gaussian", "tetra"], doc="e-DOS method")
edos_step_ev = param.Number(0.1, bounds=(1e-6, None), step=0.1, doc='e-DOS step in eV')
edos_width_ev = param.Number(0.2, step=0.05, bounds=(1e-6, None), doc='e-DOS Gaussian broadening in eV')
# SKW interpolation of the KS band energies.
skw_lpratio = param.Integer(5, bounds=(1, None))
skw_line_density = param.Integer(20)
# For the max size of file see: https://github.com/holoviz/panel/issues/1559
ebands_kpath = None
ebands_kpath_fileinput = param.FileSelector()
ebands_kmesh = None
ebands_kmesh_fileinput = param.FileSelector()
def __init__(self, ebands, **params):
self.ebands = ebands
PanelWithStructure.__init__(self, structure=ebands.structure, **params)
# Create buttons
self.plot_ebands_btn = pnw.Button(name="Plot e-bands", button_type='primary')
self.plot_edos_btn = pnw.Button(name="Plot e-DOS", button_type='primary')
self.plot_skw_btn = pnw.Button(name="Plot SKW interpolant", button_type='primary')
# Fermi surface plotter.
objects = [None, "matplotlib", "xcrysden"]
if self.has_remote_server:
objects = [None, "matplotlib"]
self.fs_viewer = param.ObjectSelector(default=None, objects=objects)
self.plot_fermi_surface_btn = pnw.Button(name="Plot Fermi surface", button_type='primary')
#ebands_kpath_fileinput = pnw.FileInput(accept=".nc")
#ebands_kmesh_fileinput = pnw.FileInput(accept=".nc")
#super().__init__(**params)
#super().__init__(structure= **params)
[docs] @pn.depends("ebands_kpath_fileinput", watch=True)
def get_ebands_kpath(self):
"""
Receives the netcdf file selected by the user as binary string.
"""
from abipy.electrons import ElectronBands
bstring = self.ebands_kpath_fileinput
self.ebands_kpath = ElectronBands.from_binary_string(bstring)
[docs] @depends_on_btn_click('plot_ebands_btn')
def on_plot_ebands_btn(self):
"""Button triggering ebands plot."""
if self.set_fermie_to_vbm:
self.ebands.set_fermie_to_vbm()
sz_mode = "stretch_width"
col = pn.Column(sizing_mode=sz_mode); ca = col.append
ca("## Electronic band structure:")
fig1 = self.ebands.plotly(e0="fermie", ylims=None, with_gaps=self.with_gaps, max_phfreq=None, show=False)
ca(ply(fig1))
ca("## Brillouin zone and **k**-path:")
kpath_pane = ply(self.ebands.kpoints.plotly(show=False), with_divider=False)
df_kpts = dfc(self.ebands.kpoints.get_highsym_datataframe(), with_divider=False)
ca(pn.Row(kpath_pane, df_kpts, sizing_mode=sz_mode))
ca(pn.layout.Divider())
#ca(bkw.PreText(text=self.ebands.to_string(verbose=self.verbose)))
return col
[docs] @depends_on_btn_click('plot_edos_btn')
def on_plot_edos_btn(self):
"""Button triggering edos plot."""
edos = self.ebands.get_edos(method=self.edos_method, step=self.edos_step_ev, width=self.edos_width_ev)
return pn.Row(ply(edos.plotly(show=False)), sizing_mode='scale_width')
[docs] @depends_on_btn_click('plot_skw_btn')
def on_plot_skw_btn(self):
"""
"""
col = pn.Column(sizing_mode='stretch_width'); ca = col.append
intp = self.ebands.interpolate(lpratio=self.skw_lpratio, line_density=self.skw_line_density,
kmesh=None, is_shift=None, bstart=0, bstop=None, filter_params=None,
verbose=self.verbose)
ca("## SKW interpolated bands along an automatically selected high-symmetry **k**-path")
ca(ply(intp.ebands_kpath.plotly(with_gaps=self.with_gaps, show=False)))
if self.ebands_kpath is not None:
ca("## Input bands taken from file uploaded by user:")
ca(ply(self.ebands_kpath.plotly(with_gaps=self.with_gaps, show=False)))
# Use line_density 0 to interpolate on the same set of k-points given in self.ebands_kpath
vertices_names = []
for kpt in self.ebands_kpath.kpoints:
vertices_names.append((kpt.frac_coords, kpt.name))
intp = self.ebands.interpolate(lpratio=self.skw_lpratio, vertices_names=vertices_names, line_density=0,
kmesh=None, is_shift=None, bstart=0, bstop=None, filter_params=None,
verbose=self.verbose)
plotter = self.ebands_kpath.get_plotter_with("Input", "Interpolated", intp.ebands_kpath)
ca("## Input bands vs SKW interpolated bands:")
#ca(mpl(plotter.combiplot(show=False)))
ca(mpl(plotter.combiplotly(show=False)))
return col
[docs] @depends_on_btn_click('plot_fermi_surface_btn')
def on_plot_fermi_surface_btn(self):
#if self.fs_viewer is None: return pn.pane.HTML()
# Cache eb3d
if hasattr(self, "_eb3d"):
eb3d = self._eb3d
else:
# Build ebands in full BZ.
eb3d = self._eb3d = self.ebands.get_ebands3d()
if self.fs_viewer == "matplotlib":
# Use matplotlib to plot isosurfaces corresponding to the Fermi level (default)
# Warning: requires skimage package, rendering could be slow.
fig = eb3d.plot_isosurfaces(e0="fermie", cmap=None, **self.mpl_kwargs)
return pn.Row(mpl(fig), sizing_mode='scale_width')
else:
raise ValueError("Invalid choice: %s" % self.fs_viewer)
#elif self.fs_viewer == "xcrysden":
# Alternatively, it's possible to export the data in xcrysden format
# and then use `xcrysden --bxsf mgb2.bxsf`
#eb3d.to_bxsf("mgb2.bxsf")
# If you have mayavi installed, try:
#eb3d.mvplot_isosurfaces()
[docs]class BaseRobotPanel(AbipyParameterized):
"""
Base class for panels with AbiPy robot.
"""
def __init__(self, robot, **params):
self.robot = robot
self.compare_params_btn = pnw.Button(name="Compare structures", button_type='primary')
self.transpose_params = pnw.Checkbox(name='Transpose table', default=True)
super().__init__(**params)
[docs] @depends_on_btn_click("compare_params_btn")
def on_compare_params_btn(self):
"""
"""
col = pn.Column(sizing_mode='stretch_width'); ca = col.append
transpose = self.transpose_params.value
dfs = self.robot.get_structure_dataframes()
ca("# Lattice dataframe")
ca(dfc(dfs.lattice, transpose=transpose))
ca("# Parameters dataframe")
ca(dfc(self.robot.get_params_dataframe(), transpose=transpose))
accord = pn.Accordion(sizing_mode='stretch_width')
accord.append(("Atomic positions", dfc(dfs.coords, transpose=transpose)))
ca(accord)
return col
# TODO: widgets to change robot labels.
[docs]class PanelWithEbandsRobot(BaseRobotPanel):
"""
Mixin class for panels with a robot that owns a list of of |ElectronBands|.
"""
def __init__(self, robot, **params):
BaseRobotPanel.__init__(self, robot=robot, **params)
# Widgets to plot ebands.
self.ebands_plotter_mode = pnw.Select(name="Plot Mode", value="gridplot",
options=["gridplot", "combiplot", "boxplot", "combiboxplot"]) # "animate",
self.ebands_plotter_btn = pnw.Button(name="Plot", button_type='primary')
self.ebands_df_checkbox = pnw.Checkbox(name='With Ebands DataFrame', value=False)
# Widgets to plot edos.
self.edos_plotter_mode = pnw.Select(name="Plot Mode", value="gridplot", options=["gridplot", "combiplot"])
self.edos_plotter_btn = pnw.Button(name="Plot", button_type='primary')
[docs] @depends_on_btn_click("ebands_plotter_btn")
def on_ebands_plotter_btn(self):
ebands_plotter = self.robot.get_ebands_plotter()
plot_mode = self.ebands_plotter_mode.value
plot_func = getattr(ebands_plotter, plot_mode, None)
if plot_func is None:
raise ValueError("Don't know how to handle plot_mode: %s" % plot_mode)
fig = plot_func(**self.mpl_kwargs)
col = pn.Column(mpl(fig), sizing_mode='scale_width')
if self.ebands_df_checkbox.value:
df = ebands_plotter.get_ebands_frame(with_spglib=True)
col.append(dfc(df))
return pn.Row(col, sizing_mode='scale_width')
[docs] @depends_on_btn_click("edos_plotter_btn")
def on_edos_plotter_btn(self):
"""Plot the electronic density of states."""
edos_plotter = self.robot.get_edos_plotter()
plot_mode = self.edos_plotter_mode.value
plot_func = getattr(edos_plotter, plot_mode, None)
if plot_func is None:
raise ValueError("Don't know how to handle plot_mode: %s" % plot_mode)
fig = plot_func(**self.mpl_kwargs)
return pn.Row(pn.Column(mpl(fig)), sizing_mode='scale_width')
[docs]def jsmol_html(structure, width=700, height=700, color="black", spin="false"):
cif_str = structure.write_cif_with_spglib_symms(None, ret_string=True) #, symprec=symprec
# There's a bug in boken when we use strings with several '" quotation marks
# To bypass the problem I create a json list of strings and then I use a js variable
# to recreate the cif file by joining the tokens.
# const elements = ['Fire', 'Air', 'Water'];
# var string = elements.join('\n');
import json
lines = cif_str.split("\n")
lines = json.dumps(lines, indent=4)
#print("lines:", lines)
jsmol_div_id = gen_id()
jsmol_app_name = "js1"
supercell = "{2, 2, 2}"
supercell = "{1, 1, 1}"
#script_str = 'load inline "%s" {1 1 1};' % cif_str
#script_str = "load $caffeine"
# http://wiki.jmol.org/index.php/Jmol_JavaScript_Object/Functions#getAppletHtml
html = f"""
<script type="text/javascript">
$(document).ready(function() {{
const lines = {lines};
var script_str = 'load inline " ' + lines.join('\\n') + '" {supercell}';
//console.log(script_str);
var Info = {{
color: "{color}",
spin: {spin},
antialiasDisplay: true,
width: {width},
height: {height},
j2sPath: "http://chemapps.stolaf.edu/jmol/jsmol/j2s",
serverURL: "http://chemapps.stolaf.edu/jmol/jsmol/php/jsmol.php",
script: script_str,
use: 'html5',
disableInitialConsole: true,
disableJ2SLoadMonitor: true,
debug: false
}};
$("#{jsmol_div_id}").html(Jmol.getAppletHtml("{jsmol_app_name}", Info));
}});
</script>
<div id="{jsmol_div_id}" style="height: 100%; width: 100%; position: relative;"></div>
"""
#print(html)
return pn.Column(pn.pane.HTML(html, sizing_mode="stretch_width"), sizing_mode="stretch_width")