diff --git a/src/aiidalab_qe_vibroscopy/app/model.py b/src/aiidalab_qe_vibroscopy/app/model.py index 0df0759..c6cf08c 100644 --- a/src/aiidalab_qe_vibroscopy/app/model.py +++ b/src/aiidalab_qe_vibroscopy/app/model.py @@ -91,7 +91,9 @@ class VibroConfigurationSettingsModel(ConfigurationSettingsModel, HasInputStruct trait=tl.Int(), default_value=[2, 2, 2], ) - supercell_number_estimator = tl.Unicode("?") + supercell_number_estimator = tl.Unicode( + "Click the button to estimate the supercell size." + ) def get_model_state(self): return { diff --git a/src/aiidalab_qe_vibroscopy/app/result.py b/src/aiidalab_qe_vibroscopy/app/result.py index 4b1d19e..d62b276 100644 --- a/src/aiidalab_qe_vibroscopy/app/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result.py @@ -9,7 +9,6 @@ import numpy as np from ..utils.raman.result import export_iramanworkchain_data -from ..utils.dielectric.result import export_dielectric_data, DielectricResults from ..utils.phonons.result import export_phononworkchain_data from ..utils.euphonic import ( @@ -86,7 +85,6 @@ def _update_view(self): spectra_data = export_iramanworkchain_data(self.node) phonon_data = export_phononworkchain_data(self.node) ins_data = export_euphonic_data(self.node) - dielectric_data = export_dielectric_data(self.node) if phonon_data: phonon_children = () @@ -208,11 +206,6 @@ def _update_view(self): ) tab_titles.append("Raman/IR spectra") - if dielectric_data: - dielectric_results = DielectricResults(dielectric_data) - children_result_widget += (dielectric_results,) - tab_titles.append("Dielectric properties") - # euphonic if ins_data: intensity_maps = EuphonicSuperWidget( diff --git a/src/aiidalab_qe_vibroscopy/app/result/result.py b/src/aiidalab_qe_vibroscopy/app/result/result.py index 849bb56..d445519 100644 --- a/src/aiidalab_qe_vibroscopy/app/result/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result/result.py @@ -3,6 +3,8 @@ from aiidalab_qe_vibroscopy.app.result.model import VibroResultsModel from aiidalab_qe.common.panel import ResultsPanel +from aiidalab_qe_vibroscopy.app.widgets.dielectricwidget import DielectricWidget +from aiidalab_qe_vibroscopy.app.widgets.dielectricmodel import DielectricModel import ipywidgets as ipw @@ -19,6 +21,10 @@ def render(self): layout=ipw.Layout(min_height="250px"), selected_index=None, ) + self.tabs.observe( + self._on_tab_change, + "selected_index", + ) tab_data = [] # vibro_node = self._model.get_vibro_node() @@ -29,8 +35,14 @@ def render(self): if self._model.needs_raman_tab(): tab_data.append(("Raman", ipw.HTML("raman_data"))) - if self._model.needs_dielectric_tab(): - tab_data.append(("Dielectric", ipw.HTML("dielectric_data"))) + dielectric_data = self._model.needs_dielectric_tab() + + if dielectric_data: + dielectric_model = DielectricModel() + dielectric_widget = DielectricWidget( + model=dielectric_model, dielectric_data=dielectric_data + ) + tab_data.append(("Dielectric Properties", dielectric_widget)) if self._model.needs_euphonic_tab(): tab_data.append(("Euphonic", ipw.HTML("euphonic_data"))) @@ -43,3 +55,8 @@ def render(self): self.children = [self.tabs] self.rendered = True + + def _on_tab_change(self, change): + if (tab_index := change["new"]) is None: + return + self.tabs.children[tab_index].render() # type: ignore diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/dielectricmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/dielectricmodel.py index 6b76f76..3fbd2d1 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/dielectricmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/dielectricmodel.py @@ -1,9 +1,156 @@ from aiidalab_qe.common.mvc import Model -from aiida.common.extendeddicts import AttributeDict import traitlets as tl +from aiidalab_qe_vibroscopy.utils.dielectric.result import NumpyEncoder +import numpy as np +import base64 +import json +from IPython.display import display -class DielectricModel(Model): - vibro = tl.Instance(AttributeDict, allow_none=True) +class DielectricModel(Model): dielectric_data = {} + + site_selector_options = tl.List( + trait=tl.Tuple((tl.Unicode(), tl.Int())), + ) + + dielectric_tensor_table = tl.Unicode("") + born_charges_table = tl.Unicode("") + raman_tensors_table = tl.Unicode("") + site = tl.Int() + + def set_initial_values(self): + """Set the initial values for the model.""" + + self.dielectric_tensor_table = self._create_dielectric_tensor_table() + self.born_charges_table = self._create_born_charges_table(0) + self.raman_tensors_table = self._create_raman_tensors_table(0) + self.site_selector_options = self._get_site_selector_options() + + def _get_site_selector_options(self): + """Get the site selector options.""" + if not self.dielectric_data: + return [] + + unit_cell_sites = self.dielectric_data["unit_cell"] + decimal_places = 5 + # Create the options with rounded positions + site_selector_options = [ + ( + f"{site.kind_name} @ ({', '.join(f'{coord:.{decimal_places}f}' for coord in site.position)})", + index, + ) + for index, site in enumerate(unit_cell_sites) + ] + return site_selector_options + + def _create_dielectric_tensor_table(self): + """Create the HTML table for the dielectric tensor.""" + if not self.dielectric_data: + return "" + + dielectric_tensor = self.dielectric_data["dielectric_tensor"] + table_data = self._generate_table(dielectric_tensor) + return table_data + + def _create_born_charges_table(self, site_index): + """Create the HTML table for the Born charges.""" + if not self.dielectric_data: + return "" + + born_charges = self.dielectric_data["born_charges"] + round_data = born_charges[site_index].round(6) + table_data = self._generate_table(round_data) + return table_data + + def _create_raman_tensors_table(self, site_index): + """Create the HTML table for the Raman tensors.""" + if not self.dielectric_data: + return "" + + raman_tensors = self.dielectric_data["raman_tensors"] + round_data = raman_tensors[site_index].round(6) + table_data = self._generate_table(round_data, cell_width="200px") + return table_data + + def download_data(self, _=None): + """Function to download the data.""" + if self.dielectric_data: + data_to_print = { + key: value + for key, value in self.dielectric_data.items() + if key != "unit_cell" + } + file_name = "dielectric_data.json" + json_str = json.dumps(data_to_print, cls=NumpyEncoder) + b64_str = base64.b64encode(json_str.encode()).decode() + self._download(payload=b64_str, filename=file_name) + + @staticmethod + def _download(payload, filename): + """Download payload as a file named as filename.""" + from IPython.display import Javascript + + javas = Javascript( + f""" + 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); + """ + ) + display(javas) + + def on_site_selection_change(self, site): + self.site = site + self.born_charges_table = self._create_born_charges_table(site) + self.raman_tensors_table = self._create_raman_tensors_table(site) + + def _generate_table(self, data, cell_width="50px"): + rows = [] + for row in data: + cells = [] + for value in row: + # Check if value is a numpy array + if isinstance(value, np.ndarray): + # Format the numpy array as a string, e.g., "[0, 0, 1]" + value_str = np.array2string( + value, separator=", ", formatter={"all": lambda x: f"{x:.6g}"} + ) + cell = f"{value_str}" + elif isinstance(value, str) and value == "special": + # Handle the "special" keyword + cell = f'{value}' + else: + # Handle other types (numbers, strings, etc.) + cell = f"{value}" + cells.append(cell) + rows.append(f"{''.join(cells)}") + + # Define the HTML with styles, using the dynamic cell width + table_html = f""" + + + {''.join(rows)} +
+ """ + return table_html diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/dielectricwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/dielectricwidget.py index 4a57b75..d729e25 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/dielectricwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/dielectricwidget.py @@ -8,13 +8,16 @@ class DielectricWidget(ipw.VBox): Widget for displaying dielectric properties results """ - def __init__(self, model: DielectricModel, dielectric_node: None, **kwargs): + def __init__(self, model: DielectricModel, dielectric_data: None, **kwargs): super().__init__( children=[LoadingWidget("Loading widgets")], **kwargs, ) self._model = model + self.rendered = False + self._model.dielectric_data = dielectric_data + def render(self): if self.rendered: return @@ -31,8 +34,76 @@ def render(self): """ ) + self.site_selector = ipw.Dropdown( + layout=ipw.Layout(width="450px"), + description="Select atom site:", + style={"description_width": "initial"}, + ) + ipw.dlink( + (self._model, "site_selector_options"), + (self.site_selector, "options"), + ) + self.site_selector.observe(self._on_site_change, names="value") + + self.download_button = ipw.Button( + description="Download Data", icon="download", button_style="primary" + ) + + self.download_button.on_click(self._model.download_data) + + # HTML table with the dielectric tensor + self.dielectric_tensor_table = ipw.HTML() + ipw.link( + (self._model, "dielectric_tensor_table"), + (self.dielectric_tensor_table, "value"), + ) + + # HTML table with the Born charges @ site + self.born_charges_table = ipw.HTML() + ipw.link( + (self._model, "born_charges_table"), + (self.born_charges_table, "value"), + ) + + # HTML table with the Raman tensors @ site + self.raman_tensors_table = ipw.HTML() + ipw.link( + (self._model, "raman_tensors_table"), + (self.raman_tensors_table, "value"), + ) + self.children = [ self.dielectric_results_help, + ipw.HTML("

Dielectric tensor

"), + self.dielectric_tensor_table, + self.site_selector, + ipw.HBox( + [ + ipw.VBox( + [ + ipw.HTML("

Born effective charges

"), + self.born_charges_table, + ] + ), + ipw.VBox( + [ + ipw.HTML("

Raman Tensor

"), + self.raman_tensors_table, + ] + ), + ] + ), + self.download_button, ] self.rendered = True + self._initial_view() + + def _initial_view(self): + self._model.set_initial_values() + self.dielectric_tensor_table.layout = ipw.Layout(width="300px", height="auto") + self.born_charges_table.layout = ipw.Layout(width="300px", height="auto") + # self.raman_tensors_table.layout = ipw.Layout(width="auto", height="auto") + + def _on_site_change(self, change): + self._model.on_site_selection_change(change["new"]) diff --git a/src/aiidalab_qe_vibroscopy/utils/dielectric/result.py b/src/aiidalab_qe_vibroscopy/utils/dielectric/result.py index 2bec7dd..2806e9c 100644 --- a/src/aiidalab_qe_vibroscopy/utils/dielectric/result.py +++ b/src/aiidalab_qe_vibroscopy/utils/dielectric/result.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -import ipywidgets as ipw -from IPython.display import HTML, display -import base64 import json import numpy as np @@ -113,172 +110,3 @@ def export_dielectric_data(node): "nlo_susceptibility": nlo_susceptibility, "unit_cell": unit_cell, } - - -class DielectricResults(ipw.VBox): - def __init__(self, dielectric_data): - # Helper - self.dielectric_results_help = ipw.HTML( - """
- The DielectricWorkchain computes different properties:
- -High Freq. Dielectric Tensor
- -Born Charges
- -Raman Tensors
- -The non-linear optical susceptibility tensor
- All information can be downloaded as a JSON file.
- -
""" - ) - - self.unit_cell_sites = dielectric_data.pop("unit_cell") - self.dielectric_data = dielectric_data - self.dielectric_tensor = dielectric_data["dielectric_tensor"] - self.born_charges = dielectric_data["born_charges"] - self.volume = dielectric_data["volume"] - self.raman_tensors = dielectric_data["raman_tensors"] - self.nlo_susceptibility = dielectric_data["nlo_susceptibility"] - - # HTML table with the dielectric tensor - self.dielectric_tensor_table = ipw.Output() - - # HTML table with the Born charges @ site - self.born_charges_table = ipw.Output() - - # HTML table with the Raman tensors @ site - self.raman_tensors_table = ipw.Output() - - decimal_places = 6 - # Create the options with rounded positions - site_selector_options = [ - ( - f"{site.kind_name} @ ({', '.join(f'{coord:.{decimal_places}f}' for coord in site.position)})", - index, - ) - for index, site in enumerate(self.unit_cell_sites) - ] - - self.site_selector = ipw.Dropdown( - options=site_selector_options, - value=site_selector_options[0][1], - layout=ipw.Layout(width="450px"), - description="Select atom site:", - style={"description_width": "initial"}, - ) - # Download button - self.download_button = ipw.Button( - description="Download Data", icon="download", button_style="primary" - ) - self.download_button.on_click(self.download_data) - - # Initialize the HTML table - self._create_dielectric_tensor_table() - # Initialize Born Charges Table - self._create_born_charges_table(self.site_selector.value) - # Initialize Raman Tensors Table - self._create_raman_tensors_table(self.site_selector.value) - - self.site_selector.observe(self._on_site_selection_change, names="value") - super().__init__( - children=( - self.dielectric_results_help, - ipw.HTML("

Dielectric tensor

"), - self.dielectric_tensor_table, - self.site_selector, - ipw.HBox( - [ - ipw.VBox( - [ - ipw.HTML("

Born effective charges

"), - self.born_charges_table, - ] - ), - ipw.VBox( - [ - ipw.HTML("

Raman Tensor

"), - self.raman_tensors_table, - ] - ), - ] - ), - self.download_button, - ) - ) - - def download_data(self, _=None): - """Function to download the data.""" - file_name = "dielectric_data.json" - - json_str = json.dumps(self.dielectric_data, cls=NumpyEncoder) - b64_str = base64.b64encode(json_str.encode()).decode() - self._download(payload=b64_str, filename=file_name) - - @staticmethod - def _download(payload, filename): - """Download payload as a file named as filename.""" - from IPython.display import Javascript - - javas = Javascript( - f""" - 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); - """ - ) - display(javas) - - def _create_html_table(self, matrix): - """ - Create an HTML table representation of a 3x3 matrix. - - :param matrix: List of lists representing a 3x3 matrix - :return: HTML table string - """ - html = '' - for row in matrix: - html += "" - for cell in row: - html += f'' - html += "" - html += "
{cell}
" - return html - - def _create_dielectric_tensor_table(self): - table_data = self._create_html_table(self.dielectric_tensor) - self.dielectric_tensor_table.layout = { - "overflow": "auto", - "height": "100px", - "width": "300px", - } - with self.dielectric_tensor_table: - display(HTML(table_data)) - - def _create_born_charges_table(self, site_index): - round_data = self.born_charges[site_index].round(6) - table_data = self._create_html_table(round_data) - self.born_charges_table.layout = { - "overflow": "auto", - "height": "150px", - "width": "300px", - } - with self.born_charges_table: - display(HTML(table_data)) - - def _create_raman_tensors_table(self, site_index): - round_data = self.raman_tensors[site_index].round(6) - table_data = self._create_html_table(round_data) - self.raman_tensors_table.layout = { - "overflow": "auto", - "height": "150px", - "width": "500px", - } - with self.raman_tensors_table: - display(HTML(table_data)) - - def _on_site_selection_change(self, change): - self.born_charges_table.clear_output() - self.raman_tensors_table.clear_output() - self._create_born_charges_table(change["new"]) - self._create_raman_tensors_table(change["new"])