diff --git a/src/aiidalab_qe_vibroscopy/app/result.py b/src/aiidalab_qe_vibroscopy/app/result.py index 2f6f710..af78e20 100644 --- a/src/aiidalab_qe_vibroscopy/app/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result.py @@ -4,61 +4,14 @@ from aiidalab_qe.common.panel import ResultPanel -from aiidalab_qe.common.bandpdoswidget import BandPdosPlotly -from IPython.display import display -import numpy as np -from ..utils.phonons.result import export_phononworkchain_data from ..utils.euphonic import ( export_euphonic_data, EuphonicSuperWidget, - DownloadYamlHdf5Widget, ) -import plotly.graph_objects as go -import ipywidgets as ipw - -class PhononBandPdosPlotly(BandPdosPlotly): - def __init__(self, bands_data=None, pdos_data=None): - super().__init__(bands_data, pdos_data) - self._bands_yaxis = go.layout.YAxis( - title=dict(text="Phonon Bands (THz)", standoff=1), - side="left", - showgrid=True, - showline=True, - zeroline=True, - range=self.SETTINGS["vertical_range_bands"], - fixedrange=False, - automargin=True, - ticks="inside", - linewidth=2, - linecolor=self.SETTINGS["axis_linecolor"], - tickwidth=2, - zerolinewidth=2, - ) - - paths = self.bands_data.get("paths") - slider_bands = go.layout.xaxis.Rangeslider( - thickness=0.08, - range=[0, paths[-1]["x"][-1]], - ) - self._bands_xaxis = go.layout.XAxis( - title="q-points", - range=[0, paths[-1]["x"][-1]], - showgrid=True, - showline=True, - tickmode="array", - rangeslider=slider_bands, - fixedrange=False, - tickvals=self.bands_data["pathlabels"][1], # ,self.band_labels[1], - ticktext=self.bands_data["pathlabels"][0], # self.band_labels[0], - showticklabels=True, - linecolor=self.SETTINGS["axis_linecolor"], - mirror=True, - linewidth=2, - type="linear", - ) +import ipywidgets as ipw class Result(ResultPanel): @@ -79,87 +32,8 @@ def _update_view(self): children_result_widget = () tab_titles = [] # this is needed to name the sub panels - phonon_data = export_phononworkchain_data(self.node) ins_data = export_euphonic_data(self.node) - if phonon_data: - phonon_children = () - if phonon_data["bands"] or phonon_data["pdos"]: - _bands_plot_view_class = PhononBandPdosPlotly( - bands_data=phonon_data["bands"][0], - pdos_data=phonon_data["pdos"][0], - ).bandspdosfigure - _bands_plot_view_class.update_layout( - yaxis=dict(autorange=True), # Automatically scale the y-axis - ) - - # the data (bands and pdos) are the first element of the lists phonon_data["bands"] and phonon_data["pdos"]! - downloadBandsPdos_widget = DownloadBandsPdosWidget( - data=phonon_data, - ) - downloadYamlHdf5_widget = DownloadYamlHdf5Widget( - phonopy_node=self.node.outputs.vibronic.phonon_pdos.creator - ) - - phonon_children += ( - _bands_plot_view_class, - ipw.HBox( - children=[ - downloadBandsPdos_widget, - downloadYamlHdf5_widget, - ] - ), - ) - - if phonon_data["thermo"]: - import plotly.graph_objects as go - - T = phonon_data["thermo"][0][0] - F = phonon_data["thermo"][0][1] - F_units = phonon_data["thermo"][0][2] - E = phonon_data["thermo"][0][3] - E_units = phonon_data["thermo"][0][4] - Cv = phonon_data["thermo"][0][5] - Cv_units = phonon_data["thermo"][0][6] - - g = go.FigureWidget( - layout=go.Layout( - title=dict(text="Thermal properties"), - barmode="overlay", - ) - ) - g.update_layout( - xaxis=dict( - title="Temperature (K)", - linecolor="black", - linewidth=2, - showline=True, - ), - yaxis=dict(linecolor="black", linewidth=2, showline=True), - plot_bgcolor="white", - ) - g.add_scatter(x=T, y=F, name=f"Helmoltz Free Energy ({F_units})") - g.add_scatter(x=T, y=E, name=f"Entropy ({E_units})") - g.add_scatter(x=T, y=Cv, name=f"Specific Heat-V=const ({Cv_units})") - - downloadThermo_widget = DownloadThermoWidget(T, F, E, Cv) - - phonon_children += ( - g, - downloadThermo_widget, - ) - - tab_titles.append("Phonon properties") - - children_result_widget += ( - ipw.VBox( - children=phonon_children, - layout=ipw.Layout( - width="100%", - ), - ), - ) # the comma is required! otherwise the tuple is not detected. - # euphonic if ins_data: intensity_maps = EuphonicSuperWidget( @@ -174,127 +48,3 @@ def _update_view(self): self.result_tabs.set_title(title_index, tab_titles[title_index]) self.children = [self.result_tabs] - - -class DownloadBandsPdosWidget(ipw.HBox): - def __init__(self, data, **kwargs): - self.download_button = ipw.Button( - description="Download phonon bands and pdos data", - icon="pencil", - button_style="primary", - disabled=False, - layout=ipw.Layout(width="auto"), - ) - self.download_button.on_click(self.download_data) - self.bands_data = data["bands"] - self.pdos_data = data["pdos"] - - super().__init__( - children=[ - self.download_button, - ], - ) - - def download_data(self, _=None): - """Function to download the phonon data.""" - import json - from monty.json import jsanitize - import base64 - - file_name_bands = "phonon_bands_data.json" - file_name_pdos = "phonon_dos_data.json" - if self.bands_data[0]: - bands_data_export = {} - for key, value in self.bands_data[0].items(): - if isinstance(value, np.ndarray): - bands_data_export[key] = value.tolist() - else: - bands_data_export[key] = value - - json_str = json.dumps(jsanitize(bands_data_export)) - b64_str = base64.b64encode(json_str.encode()).decode() - self._download(payload=b64_str, filename=file_name_bands) - if self.pdos_data: - json_str = json.dumps(jsanitize(self.pdos_data[0])) - b64_str = base64.b64encode(json_str.encode()).decode() - self._download(payload=b64_str, filename=file_name_pdos) - - @staticmethod - def _download(payload, filename): - from IPython.display import Javascript - - javas = Javascript( - """ - var link = document.createElement('a'); - link.href = 'data:text/json;charset=utf-8;base64,{payload}' - link.download = "{filename}" - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - """.format(payload=payload, filename=filename) - ) - display(javas) - - -class DownloadThermoWidget(ipw.HBox): - def __init__(self, T, F, E, Cv, **kwargs): - self.download_button = ipw.Button( - description="Download thermal properties data", - icon="pencil", - button_style="primary", - disabled=False, - layout=ipw.Layout(width="auto"), - ) - self.download_button.on_click(self.download_data) - - self.temperature = T - self.free_E = F - self.entropy = E - self.Cv = Cv - - super().__init__( - children=[ - self.download_button, - ], - ) - - def download_data(self, _=None): - """Function to download the phonon data.""" - import json - import base64 - - file_name = "phonon_thermo_data.json" - data_export = {} - for key, value in zip( - [ - "Temperature (K)", - "Helmoltz Free Energy (kJ/mol)", - "Entropy (J/K/mol)", - "Specific Heat-V=const (J/K/mol)", - ], - [self.temperature, self.free_E, self.entropy, self.Cv], - ): - if isinstance(value, np.ndarray): - data_export[key] = value.tolist() - else: - data_export[key] = value - - json_str = json.dumps(data_export) - b64_str = base64.b64encode(json_str.encode()).decode() - self._download(payload=b64_str, filename=file_name) - - @staticmethod - def _download(payload, filename): - from IPython.display import Javascript - - javas = Javascript( - """ - var link = document.createElement('a'); - link.href = 'data:text/json;charset=utf-8;base64,{payload}' - link.download = "{filename}" - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - """.format(payload=payload, filename=filename) - ) - display(javas) diff --git a/src/aiidalab_qe_vibroscopy/app/result/result.py b/src/aiidalab_qe_vibroscopy/app/result/result.py index ba8001e..8baa363 100644 --- a/src/aiidalab_qe_vibroscopy/app/result/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result/result.py @@ -10,6 +10,9 @@ from aiidalab_qe_vibroscopy.app.widgets.ir_ramanwidget import IRRamanWidget from aiidalab_qe_vibroscopy.app.widgets.ir_ramanmodel import IRRamanModel +from aiidalab_qe_vibroscopy.app.widgets.phononwidget import PhononWidget +from aiidalab_qe_vibroscopy.app.widgets.phononmodel import PhononModel + class VibroResultsPanel(ResultsPanel[VibroResultsModel]): title = "Vibronic" @@ -32,8 +35,15 @@ def render(self): tab_data = [] vibro_node = self._model.get_vibro_node() - if self._model.needs_phonons_tab(): - tab_data.append(("Phonons", ipw.HTML("phonon_data"))) + needs_phonons_tab = self._model.needs_phonons_tab() + if needs_phonons_tab: + vibroscopy_node = self._model._fetch_child_process_node() + phonon_model = PhononModel() + phonon_widget = PhononWidget( + model=phonon_model, + node=vibroscopy_node, + ) + tab_data.append(("Phonons", phonon_widget)) needs_raman_tab = self._model.needs_raman_tab() if needs_raman_tab: diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/phononmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/phononmodel.py new file mode 100644 index 0000000..41010a4 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/app/widgets/phononmodel.py @@ -0,0 +1,114 @@ +from aiidalab_qe.common.mvc import Model +import traitlets as tl + +from aiida.orm.nodes.process.workflow.workchain import WorkChainNode +from aiidalab_qe_vibroscopy.utils.phonons.result import export_phononworkchain_data +from IPython.display import display +import numpy as np + + +class PhononModel(Model): + vibro = tl.Instance(WorkChainNode, allow_none=True) + + pdos_data = {} + bands_data = {} + thermo_data = {} + + def fetch_data(self): + """Fetch the phonon data from the VibroWorkChain""" + phonon_data = export_phononworkchain_data(self.vibro) + self.pdos_data = phonon_data["pdos"][0] + self.bands_data = phonon_data["bands"][0] + self.thermo_data = phonon_data["thermo"][0] + + def update_thermo_plot(self, fig): + """Update the thermal properties plot.""" + self.temperature = self.thermo_data[0] + self.free_E = self.thermo_data[1] + F_units = self.thermo_data[2] + self.entropy = self.thermo_data[3] + E_units = self.thermo_data[4] + self.Cv = self.thermo_data[5] + Cv_units = self.thermo_data[6] + fig.update_layout( + xaxis=dict( + title="Temperature (K)", + linecolor="black", + linewidth=2, + showline=True, + ), + yaxis=dict(linecolor="black", linewidth=2, showline=True), + plot_bgcolor="white", + ) + fig.add_scatter( + x=self.temperature, y=self.free_E, name=f"Helmoltz Free Energy ({F_units})" + ) + fig.add_scatter(x=self.temperature, y=self.entropy, name=f"Entropy ({E_units})") + fig.add_scatter( + x=self.temperature, y=self.Cv, name=f"Specific Heat-V=const ({Cv_units})" + ) + + def download_thermo_data(self, _=None): + """Function to download the phonon data.""" + import json + import base64 + + file_name = "phonon_thermo_data.json" + data_export = {} + for key, value in zip( + [ + "Temperature (K)", + "Helmoltz Free Energy (kJ/mol)", + "Entropy (J/K/mol)", + "Specific Heat-V=const (J/K/mol)", + ], + [self.temperature, self.free_E, self.entropy, self.Cv], + ): + if isinstance(value, np.ndarray): + data_export[key] = value.tolist() + else: + data_export[key] = value + + json_str = json.dumps(data_export) + b64_str = base64.b64encode(json_str.encode()).decode() + self._download(payload=b64_str, filename=file_name) + + def download_bandspdos_data(self, _=None): + """Function to download the phonon data.""" + import json + from monty.json import jsanitize + import base64 + + file_name_bands = "phonon_bands_data.json" + file_name_pdos = "phonon_dos_data.json" + if self.bands_data: + bands_data_export = {} + for key, value in self.bands_data.items(): + if isinstance(value, np.ndarray): + bands_data_export[key] = value.tolist() + else: + bands_data_export[key] = value + + json_str = json.dumps(jsanitize(bands_data_export)) + b64_str = base64.b64encode(json_str.encode()).decode() + self._download(payload=b64_str, filename=file_name_bands) + if self.pdos_data: + json_str = json.dumps(jsanitize(self.pdos_data)) + b64_str = base64.b64encode(json_str.encode()).decode() + self._download(payload=b64_str, filename=file_name_pdos) + + @staticmethod + def _download(payload, filename): + from IPython.display import Javascript + + javas = Javascript( + """ + var link = document.createElement('a'); + link.href = 'data:text/json;charset=utf-8;base64,{payload}' + link.download = "{filename}" + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + """.format(payload=payload, filename=filename) + ) + display(javas) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/phononwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/phononwidget.py index e69de29..a951dd8 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/phononwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/phononwidget.py @@ -0,0 +1,74 @@ +import ipywidgets as ipw + +from aiidalab_qe.common.widgets import LoadingWidget +from aiidalab_qe_vibroscopy.app.widgets.phononmodel import PhononModel + +import plotly.graph_objects as go +from aiidalab_qe.common.bands_pdos.bandpdosplotly import BandsPdosPlotly + + +class PhononWidget(ipw.VBox): + """ + Widget for displaying phonon properties results + """ + + def __init__(self, model: PhononModel, node: None, **kwargs): + super().__init__( + children=[LoadingWidget("Loading widgets")], + **kwargs, + ) + self._model = model + self._model.vibro = node + self.rendered = False + + def render(self): + if self.rendered: + return + + self.bandspdos_download_button = ipw.Button( + description="Download phonon bands and dos data", + icon="pencil", + button_style="primary", + layout=ipw.Layout(width="300px"), + ) + self.bandspdos_download_button.on_click(self._model.download_bandspdos_data) + + self.thermal_plot = go.FigureWidget( + layout=go.Layout( + title=dict(text="Thermal properties"), + barmode="overlay", + ) + ) + + self.thermo_download_button = ipw.Button( + description="Download thermal properties data", + icon="pencil", + button_style="primary", + layout=ipw.Layout(width="300px"), + ) + self.thermo_download_button.on_click(self._model.download_thermo_data) + + self.children = [ + self.bandspdos_download_button, + self.thermal_plot, + self.thermo_download_button, + ] + + self.rendered = True + self._init_view() + + def _init_view(self): + self._model.fetch_data() + self.bands_pdos = BandsPdosPlotly( + self._model.bands_data, self._model.pdos_data + ).bandspdosfigure + y_max = max(self.bands_pdos.data[0].y) + y_min = min(self.bands_pdos.data[0].y) + x_max = max(self.bands_pdos.data[1].x) + self.bands_pdos.update_layout( + xaxis=dict(title="q-points"), + yaxis=dict(title="Phonon Bands (THz)", range=[y_min - 0.1, y_max + 0.1]), + yaxis2=dict(range=[0, x_max + 0.1]), + ) + self.children = (self.bands_pdos, *self.children) + self._model.update_thermo_plot(self.thermal_plot)