diff --git a/pyproject.toml b/pyproject.toml
index c8381cb..d049066 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,12 +20,11 @@ requires-python = ">=3.8"
dependencies = [
"aiida-vibroscopy>=1.0.2",
"aiida-phonopy>=1.1.3",
- "phonopy",
- #"euphonic==1.1.0",
+ "phonopy=2.25.0",
"pre-commit",
"euphonic",
"kaleido",
- "weas-widget==0.1.15",
+ "weas-widget==0.1.19",
]
[tool.ruff.lint]
diff --git a/src/aiidalab_qe_vibroscopy/app/__init__.py b/src/aiidalab_qe_vibroscopy/app/__init__.py
index d4e2de5..361de09 100644
--- a/src/aiidalab_qe_vibroscopy/app/__init__.py
+++ b/src/aiidalab_qe_vibroscopy/app/__init__.py
@@ -1,43 +1,34 @@
-from aiidalab_qe_vibroscopy.app.settings import Setting
-from aiidalab_qe_vibroscopy.app.workchain import workchain_and_builder
-from aiidalab_qe_vibroscopy.app.result import Result
-from aiidalab_qe.common.panel import OutlinePanel
+from aiidalab_qe.common.panel import PluginOutline
-from aiidalab_qe.common.widgets import (
- QEAppComputationalResourcesWidget,
- PwCodeResourceSetupWidget,
+from aiidalab_qe_vibroscopy.app.model import VibroConfigurationSettingsModel
+from aiidalab_qe_vibroscopy.app.settings import VibroConfigurationSettingPanel
+from aiidalab_qe_vibroscopy.app.code import (
+ VibroResourceSettingsModel,
+ VibroResourcesSettingsPanel,
)
+from aiidalab_qe_vibroscopy.app.result.result import VibroResultsPanel
+from aiidalab_qe_vibroscopy.app.result.model import VibroResultsModel
+from aiidalab_qe_vibroscopy.app.workchain import workchain_and_builder
-class Outline(OutlinePanel):
- title = "Vibrational properties"
- # description = "IR and Raman spectra; you may also select phononic and dielectric properties"
+class VibroPluginOutline(PluginOutline):
+ title = "Vibrational Spectroscopy (VIBRO)"
-PhononWorkChainPwCode = PwCodeResourceSetupWidget(
- description="pw.x for phonons", # code for the PhononWorkChain workflow",
- default_calc_job_plugin="quantumespresso.pw",
-)
-
-# The finite electric field does not support npools (does not work with npools>1), so we just set it as QEAppComputationalResourcesWidget
-DielectricWorkChainPwCode = QEAppComputationalResourcesWidget(
- description="pw.x for dielectric", # code for the DielectricWorChain workflow",
- default_calc_job_plugin="quantumespresso.pw",
-)
-
-PhonopyCalculationCode = QEAppComputationalResourcesWidget(
- description="phonopy", # code for the PhonopyCalculation calcjob",
- default_calc_job_plugin="phonopy.phonopy",
-)
property = {
- "outline": Outline,
+ "outline": VibroPluginOutline,
+ "configuration": {
+ "panel": VibroConfigurationSettingPanel,
+ "model": VibroConfigurationSettingsModel,
+ },
"code": {
- "phonon": PhononWorkChainPwCode,
- "dielectric": DielectricWorkChainPwCode,
- "phonopy": PhonopyCalculationCode,
+ "panel": VibroResourcesSettingsPanel,
+ "model": VibroResourceSettingsModel,
+ },
+ "result": {
+ "panel": VibroResultsPanel,
+ "model": VibroResultsModel,
},
- "setting": Setting,
"workchain": workchain_and_builder,
- "result": Result,
}
diff --git a/src/aiidalab_qe_vibroscopy/app/code.py b/src/aiidalab_qe_vibroscopy/app/code.py
new file mode 100644
index 0000000..53574d6
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/code.py
@@ -0,0 +1,29 @@
+from aiidalab_qe.common.code.model import CodeModel, PwCodeModel
+from aiidalab_qe.common.panel import ResourceSettingsModel, ResourceSettingsPanel
+
+
+class VibroResourceSettingsModel(ResourceSettingsModel):
+ """Resource settings for the vibroscopy calculations."""
+
+ codes = {
+ "phonon": PwCodeModel(
+ description="pw.x for phonons",
+ default_calc_job_plugin="quantumespresso.pw",
+ ),
+ "dielectric": PwCodeModel(
+ description="pw.x for dielectric",
+ default_calc_job_plugin="quantumespresso.pw",
+ ),
+ "phonopy": CodeModel(
+ name="phonopy",
+ description="phonopy",
+ default_calc_job_plugin="phonopy.phonopy",
+ ),
+ }
+
+
+class VibroResourcesSettingsPanel(ResourceSettingsPanel[VibroResourceSettingsModel]):
+ """Panel for the resource settings for the vibroscopy calculations."""
+
+ title = "Vibronic"
+ identifier = identifier = "vibronic"
diff --git a/src/aiidalab_qe_vibroscopy/app/model.py b/src/aiidalab_qe_vibroscopy/app/model.py
new file mode 100644
index 0000000..86b7d3a
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/model.py
@@ -0,0 +1,221 @@
+import traitlets as tl
+
+import numpy as np
+from aiidalab_qe.common.mixins import HasInputStructure
+from aiidalab_qe.common.panel import ConfigurationSettingsModel
+
+from aiida_phonopy.data.preprocess import PreProcessData
+from aiida.plugins import DataFactory
+import sys
+import os
+
+HubbardStructureData = DataFactory("quantumespresso.hubbard_structure")
+from aiida_vibroscopy.calculations.spectra_utils import get_supercells_for_hubbard
+from aiida_vibroscopy.workflows.phonons.base import get_supercell_hubbard_structure
+
+# spinner for waiting time (supercell estimations)
+spinner_html = """
+
+
+"""
+
+
+def disable_print(func):
+ def wrapper(*args, **kwargs):
+ # Save the current standard output
+ original_stdout = sys.stdout
+ # Redirect standard output to os.devnull
+ sys.stdout = open(os.devnull, "w")
+ try:
+ # Call the function
+ result = func(*args, **kwargs)
+ finally:
+ # Restore the original standard output
+ sys.stdout.close()
+ sys.stdout = original_stdout
+ return result
+
+ return wrapper
+
+
+class VibroConfigurationSettingsModel(ConfigurationSettingsModel, HasInputStructure):
+ dependencies = [
+ "input_structure",
+ ]
+
+ simulation_type_options = tl.List(
+ trait=tl.List(tl.Union([tl.Unicode(), tl.Int()])),
+ default_value=[
+ ("IR/Raman, Phonon, Dielectric, INS properties", 1),
+ ("IR/Raman and Dielectric in Primitive Cell Approach", 2),
+ ("Phonons for non-polar materials and INS", 3),
+ ("Dielectric properties", 4),
+ ],
+ )
+ simulation_type = tl.Int(1)
+
+ symmetry_symprec = tl.Float(1e-5)
+ supercell_x = tl.Int(2)
+ supercell_y = tl.Int(2)
+ supercell_z = tl.Int(2)
+
+ # Control for disable the supercell widget
+
+ disable_x = tl.Bool(False)
+ disable_y = tl.Bool(False)
+ disable_z = tl.Bool(False)
+
+ supercell = tl.List(
+ trait=tl.Int(),
+ default_value=[2, 2, 2],
+ )
+ supercell_number_estimator = tl.Unicode(
+ "Click the button to estimate the supercell size."
+ )
+
+ def get_model_state(self):
+ return {
+ "simulation_type": self.simulation_type,
+ "symmetry_symprec": self.symmetry_symprec,
+ "supercell": self.supercell,
+ }
+
+ def set_model_state(self, parameters: dict):
+ self.simulation_type = parameters.get("simulation_type", 1)
+ self.symmetry_symprec = parameters.get("symmetry_symprec", 1e-5)
+ self.supercell = parameters.get("supercell", [2, 2, 2])
+ self.supercell_x, self.supercell_y, self.supercell_z = self.supercell
+
+ def reset(self):
+ with self.hold_trait_notifications():
+ self.simulation_type = 1
+ self.symmetry_symprec = self._get_default("symmetry_symprec")
+ self.supercell = [2, 2, 2]
+ self.supercell_x, self.supercell_y, self.supercell_z = self.supercell
+ self.supercell_number_estimator = self._get_default(
+ "supercell_number_estimator"
+ )
+
+ def _get_default(self, trait):
+ return self._defaults.get(trait, self.traits()[trait].default_value)
+
+ def on_input_structure_change(self, _=None):
+ if not self.input_structure:
+ self.reset()
+
+ else:
+ self.disable_x, self.disable_y, self.disable_z = True, True, True
+ pbc = self.input_structure.pbc
+
+ if pbc == (False, False, False):
+ # No periodicity; fully disable and reset supercell
+ self.supercell_x = self.supercell_y = self.supercell_z = 1
+ elif pbc == (True, False, False):
+ self.supercell_y = self.supercell_z = 1
+ self.disable_x = False
+ self.symmetry_symprec = 1e-3
+ elif pbc == (True, True, False):
+ self.supercell_z = 1
+ self.disable_x = self.disable_y = False
+ elif pbc == (True, True, True):
+ self.disable_x = self.disable_y = self.disable_z = False
+
+ self.supercell = [self.supercell_x, self.supercell_y, self.supercell_z]
+
+ def suggest_supercell(self, _=None):
+ """
+ minimal supercell size for phonons, imposing a minimum lattice parameter of 15 A.
+ """
+ if self.input_structure and self.input_structure.pbc != (False, False, False):
+ ase_structure = self.input_structure.get_ase()
+ suggested_3D = 15 // np.array(ase_structure.cell.cellpar()[:3]) + 1
+
+ # Update only dimensions that are not disabled
+ if not self.disable_x:
+ self.supercell_x = int(suggested_3D[0])
+ if not self.disable_y:
+ self.supercell_y = int(suggested_3D[1])
+ if not self.disable_z:
+ self.supercell_z = int(suggested_3D[2])
+
+ # Sync the updated values to the supercell list
+ self.supercell = [self.supercell_x, self.supercell_y, self.supercell_z]
+
+ else:
+ return
+
+ def supercell_reset(self, _=None):
+ if not self.disable_x:
+ self.supercell_x = self._get_default("supercell_x")
+ if not self.disable_y:
+ self.supercell_y = self._get_default("supercell_x")
+ if not self.disable_z:
+ self.supercell_z = self._get_default("supercell_x")
+ self.supercell = [self.supercell_x, self.supercell_y, self.supercell_z]
+
+ def reset_symprec(self, _=None):
+ self.symmetry_symprec = (
+ self._get_default("symmetry_symprec")
+ if self.input_structure.pbc != (True, False, False)
+ else 1e-3
+ )
+ self.supercell_number_estimator = self._get_default(
+ "supercell_number_estimator"
+ )
+
+ @disable_print
+ def _estimate_supercells(self, _=None):
+ if self.input_structure:
+ self.supercell_number_estimator = spinner_html
+
+ preprocess_data = PreProcessData(
+ structure=self.input_structure,
+ supercell_matrix=[
+ [self.supercell_x, 0, 0],
+ [0, self.supercell_y, 0],
+ [0, 0, self.supercell_z],
+ ],
+ symprec=self.symmetry_symprec,
+ distinguish_kinds=False,
+ is_symmetry=True,
+ )
+
+ if isinstance(self.input_structure, HubbardStructureData):
+ supercell = get_supercell_hubbard_structure(
+ self.input_structure,
+ self.input_structure,
+ metadata={"store_provenance": False},
+ )
+ supercells = get_supercells_for_hubbard(
+ preprocess_data=preprocess_data,
+ ref_structure=supercell,
+ metadata={"store_provenance": False},
+ )
+ else:
+ supercells = preprocess_data.get_supercells_with_displacements()
+
+ self.supercell_number_estimator = f"{len(supercells)}"
+
+ return
diff --git a/src/aiidalab_qe_vibroscopy/app/result.py b/src/aiidalab_qe_vibroscopy/app/result.py
index 4b1d19e..af78e20 100644
--- a/src/aiidalab_qe_vibroscopy/app/result.py
+++ b/src/aiidalab_qe_vibroscopy/app/result.py
@@ -4,65 +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.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 (
export_euphonic_data,
EuphonicSuperWidget,
- DownloadYamlHdf5Widget,
)
-import plotly.graph_objects as go
-import ipywidgets as ipw
-
-from ..utils.raman.result import SpectrumPlotWidget, ActiveModesWidget
-
-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):
@@ -83,135 +32,7 @@ def _update_view(self):
children_result_widget = ()
tab_titles = [] # this is needed to name the sub panels
- 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 = ()
- 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.
-
- if spectra_data:
- # Here we should provide the possibility to have both IR and Raman,
- # as the new logic can provide both at the same time.
- # We are gonna use the same widget, providing the correct spectrum_type: "Raman" or "Ir".
- children_spectra = ()
- for spectrum, data in spectra_data.items():
- if not data:
- continue
-
- elif isinstance(data, str):
- # No Modes are detected. So we explain why
- no_mode_widget = ipw.HTML(data)
- explanation_widget = ipw.HTML(
- "This may be due to the fact that the current implementation of aiida-vibroscopy plugin only considers first-order effects."
- )
-
- children_spectra += (
- ipw.VBox([no_mode_widget, explanation_widget]),
- )
-
- else:
- subwidget_title = ipw.HTML(f"{spectrum} spectroscopy
")
- spectrum_widget = SpectrumPlotWidget(
- node=self.node, output_node=data, spectrum_type=spectrum
- )
- modes_animation = ActiveModesWidget(
- node=self.node, output_node=data, spectrum_type=spectrum
- )
-
- children_spectra += (
- ipw.VBox([subwidget_title, spectrum_widget, modes_animation]),
- )
- children_result_widget += (
- ipw.VBox(
- children=children_spectra,
- layout=ipw.Layout(
- width="100%",
- ),
- ),
- )
- 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:
@@ -227,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/__init__.py b/src/aiidalab_qe_vibroscopy/app/result/__init__.py
new file mode 100644
index 0000000..6dba6bb
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/result/__init__.py
@@ -0,0 +1,7 @@
+from aiidalab_qe_vibroscopy.app.result.model import VibroResultsModel
+from aiidalab_qe_vibroscopy.app.result.result import VibroResultsPanel
+
+__all__ = [
+ "VibroResultsModel",
+ "VibroResultsPanel",
+]
diff --git a/src/aiidalab_qe_vibroscopy/app/result/model.py b/src/aiidalab_qe_vibroscopy/app/result/model.py
new file mode 100644
index 0000000..a723014
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/result/model.py
@@ -0,0 +1,41 @@
+from aiidalab_qe.common.panel import ResultsModel
+import traitlets as tl
+
+
+class VibroResultsModel(ResultsModel):
+ identifier = "vibronic"
+
+ _this_process_label = "VibroWorkChain"
+
+ tab_titles = tl.List([])
+
+ def get_vibro_node(self):
+ return self._get_child_outputs()
+
+ def needs_dielectric_tab(self):
+ node = self.get_vibro_node()
+ if not any(key in node for key in ["iraman", "dielectric", "harmonic"]):
+ return False
+ return True
+
+ def needs_raman_tab(self):
+ node = self.get_vibro_node()
+ if not any(key in node for key in ["iraman", "harmonic"]):
+ return False
+ return True
+
+ # Here we use _fetch_child_process_node() since the function needs the input_structure in inputs
+ def needs_phonons_tab(self):
+ node = self.get_vibro_node()
+ if not any(
+ key in node for key in ["phonon_bands", "phonon_thermo", "phonon_pdos"]
+ ):
+ return False
+ return True
+
+ # Here we use _fetch_child_process_node() since the function needs the input_structure in inputs
+ def needs_euphonic_tab(self):
+ node = self.get_vibro_node()
+ if not any(key in node for key in ["phonon_bands"]):
+ return False
+ return True
diff --git a/src/aiidalab_qe_vibroscopy/app/result/result.py b/src/aiidalab_qe_vibroscopy/app/result/result.py
new file mode 100644
index 0000000..8ad8d69
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/result/result.py
@@ -0,0 +1,101 @@
+"""Vibronic results view widgets"""
+
+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
+
+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
+
+from aiidalab_qe_vibroscopy.app.widgets.euphonicwidget import (
+ EuphonicSuperWidget as EuphonicWidget,
+)
+from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import (
+ EuphonicBaseResultsModel as EuphonicModel,
+)
+# from aiidalab_qe_vibroscopy.app.widgets.euphonic.model import EuphonicModel
+# from aiidalab_qe_vibroscopy.app.widgets.euphonic.widget import EuphonicWidget
+
+
+class VibroResultsPanel(ResultsPanel[VibroResultsModel]):
+ title = "Vibronic"
+ identifier = "vibronic"
+ workchain_labels = ["vibro"]
+
+ def render(self):
+ if self.rendered:
+ return
+
+ self.tabs = ipw.Tab(
+ 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()
+
+ 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:
+ vibroscopy_node = self._model._fetch_child_process_node()
+ input_structure = vibroscopy_node.inputs.structure.get_ase()
+ irraman_model = IRRamanModel()
+ irraman_widget = IRRamanWidget(
+ model=irraman_model,
+ node=vibro_node,
+ input_structure=input_structure,
+ )
+
+ tab_data.append(("Raman/IR spectra", irraman_widget))
+
+ needs_dielectric_tab = self._model.needs_dielectric_tab()
+ if needs_dielectric_tab:
+ dielectric_model = DielectricModel()
+ dielectric_widget = DielectricWidget(
+ model=dielectric_model,
+ node=vibro_node,
+ )
+ tab_data.append(("Dielectric Properties", dielectric_widget))
+
+ needs_euphonic_tab = False # self._model.needs_euphonic_tab()
+ if needs_euphonic_tab:
+ euphonic_model = EuphonicModel()
+ euphonic_widget = EuphonicWidget(
+ model=euphonic_model,
+ node=vibro_node,
+ )
+ tab_data.append(("Neutron scattering", euphonic_widget))
+
+ # Assign children and titles dynamically
+ self.tabs.children = [content for _, content in tab_data]
+
+ for index, (title, _) in enumerate(tab_data):
+ self.tabs.set_title(index, title)
+
+ self.children = [self.tabs]
+ self.rendered = True
+ self.tabs.selected_index = 0
+
+ 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/settings.py b/src/aiidalab_qe_vibroscopy/app/settings.py
index f4992a5..47ef5a4 100644
--- a/src/aiidalab_qe_vibroscopy/app/settings.py
+++ b/src/aiidalab_qe_vibroscopy/app/settings.py
@@ -8,80 +8,33 @@
"""
import ipywidgets as ipw
-import traitlets as tl
-import numpy as np
-from aiida import orm
-from aiidalab_qe.common.panel import Panel
-
-
-import sys
-import os
+from aiidalab_qe.common.panel import ConfigurationSettingsPanel
+from aiidalab_qe_vibroscopy.app.model import VibroConfigurationSettingsModel
from aiida.plugins import DataFactory
HubbardStructureData = DataFactory("quantumespresso.hubbard_structure")
-# spinner for waiting time (supercell estimations)
-spinner_html = """
-
-
-"""
-
-
-def disable_print(func):
- def wrapper(*args, **kwargs):
- # Save the current standard output
- original_stdout = sys.stdout
- # Redirect standard output to os.devnull
- sys.stdout = open(os.devnull, "w")
- try:
- # Call the function
- result = func(*args, **kwargs)
- finally:
- # Restore the original standard output
- sys.stdout.close()
- sys.stdout = original_stdout
- return result
-
- return wrapper
-
-class Setting(Panel):
+class VibroConfigurationSettingPanel(
+ ConfigurationSettingsPanel[VibroConfigurationSettingsModel],
+):
title = "Vibrational Settings"
+ identifier = "vibronic"
- simulation_mode = [
- ("IR/Raman, Phonon, Dielectric, INS properties", 1),
- ("IR/Raman and Dielectric in Primitive Cell Approach", 2),
- ("Phonons for non-polar materials and INS", 3),
- ("Dielectric properties", 4),
- ]
+ def __init__(self, model: VibroConfigurationSettingsModel, **kwargs):
+ super().__init__(model, **kwargs)
- input_structure = tl.Instance(orm.StructureData, allow_none=True)
+ self._model.observe(
+ self._on_input_structure_change,
+ "input_structure",
+ )
+
+ def render(self):
+ if self.rendered:
+ return
- def __init__(self, **kwargs):
self.settings_title = ipw.HTML(
"""
Vibrational Settings
"""
@@ -118,46 +71,71 @@ def __init__(self, **kwargs):
""",
)
- self.calc_options_description = ipw.HTML("Select calculation:")
- self.calc_options = ipw.Dropdown(
- options=self.simulation_mode,
+ self.simulation_type = ipw.Dropdown(
layout=ipw.Layout(width="450px"),
- value=self.simulation_mode[0][1],
+ )
+ ipw.dlink(
+ (self._model, "simulation_type_options"),
+ (self.simulation_type, "options"),
+ )
+ ipw.link(
+ (self._model, "simulation_type"),
+ (self.simulation_type, "value"),
+ )
+ self.simulation_type.observe(
+ self._on_change_simulation_type,
+ "value",
)
- self.calc_options.observe(self._display_supercell, names="value")
-
- # start Supercell
-
- self.supercell = [2, 2, 2]
-
- def change_supercell(_=None):
- self.supercell = [
- self._sc_x.value,
- self._sc_y.value,
- self._sc_z.value,
- ]
+ self.symmetry_symprec = ipw.BoundedFloatText(
+ max=1,
+ min=1e-7,
+ step=1e-4,
+ description="Symmetry tolerance (symprec):",
+ style={"description_width": "initial"},
+ layout={"width": "300px"},
+ )
+ ipw.link(
+ (self._model, "symmetry_symprec"),
+ (self.symmetry_symprec, "value"),
+ )
- if self.input_structure:
- pbc = self.input_structure.pbc
- else:
- pbc = (True, True, True)
-
- for elem, periodic in zip(["x", "y", "z"], pbc):
- # periodic allows support of hints also for 2D, 1D.
- setattr(
- self,
- "_sc_" + elem,
- ipw.BoundedIntText(
- value=2 if periodic else 1,
- min=1,
- layout={"width": "40px"},
- disabled=False if periodic else True,
- ),
- )
- for elem in [self._sc_x, self._sc_y, self._sc_z]:
- elem.observe(change_supercell, names="value")
- elem.observe(self._activate_estimate_supercells, names="value")
+ self.supercell_x = ipw.BoundedIntText(
+ min=1,
+ layout={"width": "40px"},
+ )
+ self.supercell_y = ipw.BoundedIntText(
+ min=1,
+ layout={"width": "40px"},
+ )
+ self.supercell_z = ipw.BoundedIntText(
+ min=1,
+ layout={"width": "40px"},
+ )
+ ipw.link(
+ (self._model, "supercell_x"),
+ (self.supercell_x, "value"),
+ )
+ ipw.link(
+ (self._model, "disable_x"),
+ (self.supercell_x, "disabled"),
+ )
+ ipw.link(
+ (self._model, "supercell_y"),
+ (self.supercell_y, "value"),
+ )
+ ipw.link(
+ (self._model, "disable_y"),
+ (self.supercell_y, "disabled"),
+ )
+ ipw.link(
+ (self._model, "supercell_z"),
+ (self.supercell_z, "value"),
+ )
+ ipw.link(
+ (self._model, "disable_z"),
+ (self.supercell_z, "disabled"),
+ )
self.supercell_selector = ipw.HBox(
children=[
@@ -167,9 +145,9 @@ def change_supercell(_=None):
)
]
+ [
- self._sc_x,
- self._sc_y,
- self._sc_z,
+ self.supercell_x,
+ self.supercell_y,
+ self.supercell_z,
],
)
@@ -182,8 +160,9 @@ def change_supercell(_=None):
layout=ipw.Layout(width="100px"),
button_style="info",
)
+
# supercell hint (15A lattice params)
- self.supercell_hint_button.on_click(self._suggest_supercell)
+ self.supercell_hint_button.on_click(self._model.suggest_supercell)
# reset supercell
self.supercell_reset_button = ipw.Button(
@@ -193,26 +172,38 @@ def change_supercell(_=None):
button_style="warning",
)
# supercell reset reaction
- self.supercell_reset_button.on_click(self._reset_supercell)
+ self.supercell_reset_button.on_click(self._model.supercell_reset)
- # Estimate supercell button
- self.supercell_estimate_button = ipw.Button(
- description="Estimate number of supercells ➡",
+ self.symmetry_symprec_reset_button = ipw.Button(
+ description="Reset symprec",
disabled=False,
- layout=ipw.Layout(width="240px", display="none"),
- button_style="info",
- tooltip="Number of supercells for phonons calculations;\nwarning: for large systems, this may take some time.",
+ layout=ipw.Layout(width="125px"),
+ button_style="warning",
)
# supercell reset reaction
- self.supercell_estimate_button.on_click(self._estimate_supercells)
+ self.symmetry_symprec_reset_button.on_click(self._model.reset_symprec)
# Estimate the number of supercells for frozen phonons.
self.supercell_number_estimator = ipw.HTML(
- # description="Number of supercells:",
- value="?",
style={"description_width": "initial"},
layout=ipw.Layout(display="none"),
)
+ ipw.link(
+ (self._model, "supercell_number_estimator"),
+ (self.supercell_number_estimator, "value"),
+ )
+
+ # Estimate supercell button
+ self.supercell_estimate_button = ipw.Button(
+ description="Estimate number of supercells ➡",
+ disabled=False,
+ layout=ipw.Layout(width="240px", display="none"),
+ button_style="info",
+ tooltip="Number of supercells for phonons calculations;\nwarning: for large systems, this may take some time.",
+ )
+
+ # supercell reset reaction
+ self.supercell_estimate_button.on_click(self._model._estimate_supercells)
## end supercell hint.
@@ -233,26 +224,9 @@ def change_supercell(_=None):
self.supercell_widget.layout.display = "block"
# end Supercell.
- self.symmetry_symprec = ipw.FloatText(
- value=1e-5,
- max=1,
- min=1e-7, # Ensure the value is always positive
- step=1e-4, # Step value of 1e-4
- description="Symmetry tolerance (symprec):",
- style={"description_width": "initial"},
- layout={"width": "300px"},
- )
- self.symmetry_symprec.observe(self._activate_estimate_supercells, "value")
+ # self.symmetry_symprec.observe(self._activate_estimate_supercells, "value")
# reset supercell
- self.symmetry_symprec_reset_button = ipw.Button(
- description="Reset symprec",
- disabled=False,
- layout=ipw.Layout(width="125px"),
- button_style="warning",
- )
- # supercell reset reaction
- self.symmetry_symprec_reset_button.on_click(self._reset_symprec)
self.children = [
ipw.VBox(
@@ -273,8 +247,8 @@ def change_supercell(_=None):
),
ipw.HBox(
[
- self.calc_options_description,
- self.calc_options,
+ ipw.HTML("Select calculation:"),
+ self.simulation_type,
],
),
self.supercell_widget,
@@ -286,157 +260,26 @@ def change_supercell(_=None):
),
]
- super().__init__(**kwargs)
-
- # we define a block for the estimation of the supercell if we ask for hint,
- # so that we call the estimator only at the end of the supercell hint generator,
- # and now each time after the x, y, z generation (i.e., we don't lose time).
- # see the methods below.
- self.block = False
-
- @tl.observe("input_structure")
- def _update_input_structure(self, change):
- if self.input_structure:
- for direction, periodic in zip(
- [self._sc_x, self._sc_y, self._sc_z], self.input_structure.pbc
- ):
- direction.value = 2 if periodic else 1
- direction.disabled = False if periodic else True
-
- self.supercell_number_estimator.layout.display = (
- "block" if len(self.input_structure.sites) <= 30 else "none"
- )
- self.supercell_estimate_button.layout.display = (
- "block" if len(self.input_structure.sites) <= 30 else "none"
- )
- else:
- self.supercell_number_estimator.layout.display = "none"
- self.supercell_estimate_button.layout.display = "none"
-
- def _display_supercell(self, change):
- selected = change["new"]
- if selected in [1, 3]:
- self.supercell_widget.layout.display = "block"
- else:
- self.supercell_widget.layout.display = "none"
-
- def _suggest_supercell(self, _=None):
- """
- minimal supercell size for phonons, imposing a minimum lattice parameter of 15 A.
- """
- if self.input_structure:
- s = self.input_structure.get_ase()
- suggested_3D = 15 // np.array(s.cell.cellpar()[:3]) + 1
-
- # if disabled, it means that it is a non-periodic direction.
- # here we manually unobserve the `_activate_estimate_supercells`, so it is faster
- # and only compute when all the three directions are updated
- self.block = True
- for direction, suggested, original in zip(
- [self._sc_x, self._sc_y, self._sc_z], suggested_3D, s.cell.cellpar()[:3]
- ):
- direction.value = suggested if not direction.disabled else 1
- self.block = False
- self._activate_estimate_supercells()
- else:
- return
-
- def _activate_estimate_supercells(self, _=None):
- self.supercell_estimate_button.disabled = False
- self.supercell_number_estimator.value = "?"
-
- # @tl.observe("input_structure")
- @disable_print
- def _estimate_supercells(self, _=None):
- """_summary_
-
- Estimate the number of supercells to be computed for frozen phonon calculation.
- """
- if self.block:
- return
-
- symprec_value = self.symmetry_symprec.value
-
- self.symmetry_symprec.value = max(1e-5, min(symprec_value, 1))
-
- self.supercell_number_estimator.value = spinner_html
+ self.rendered = True
+ self._on_change_simulation_type({"new": 1})
- from aiida_phonopy.data.preprocess import PreProcessData
-
- if self.input_structure:
- preprocess_data = PreProcessData(
- structure=self.input_structure,
- supercell_matrix=[
- [self._sc_x.value, 0, 0],
- [0, self._sc_y.value, 0],
- [0, 0, self._sc_z.value],
- ],
- symprec=self.symmetry_symprec.value,
- distinguish_kinds=False,
- is_symmetry=True,
- )
-
- supercells = preprocess_data.get_supercells_with_displacements()
-
- # for now, we comment the following part, as the HubbardSD is generated in the submission step.
- """if isinstance(self.input_structure, HubbardStructureData):
- from aiida_vibroscopy.calculations.spectra_utils import get_supercells_for_hubbard
- from aiida_vibroscopy.workflows.phonons.base import get_supercell_hubbard_structure
- supercell = get_supercell_hubbard_structure(
- self.input_structure,
- self.input_structure,
- metadata={"store_provenance": False},
- )
- supercells = get_supercells_for_hubbard(
- preprocess_data=preprocess_data,
- ref_structure=supercell,
- metadata={"store_provenance": False},
- )
+ def _on_input_structure_change(self, _):
+ self.refresh(specific="structure")
+ self._model.on_input_structure_change()
- else:
- supercells = preprocess_data.get_supercells_with_displacements()
- """
- self.supercell_number_estimator.value = f"{len(supercells)}"
- self.supercell_estimate_button.disabled = True
-
- return
-
- def _reset_supercell(self, _=None):
- if self.input_structure is not None:
- reset_supercell = []
- self.block = True
- for direction, periodic in zip(
- [self._sc_x, self._sc_y, self._sc_z], self.input_structure.pbc
- ):
- reset_supercell.append(2 if periodic else 1)
- (self._sc_x.value, self._sc_y.value, self._sc_z.value) = reset_supercell
- self.block = False
- self._activate_estimate_supercells()
- return
-
- def _reset_symprec(self, _=None):
- self.symmetry_symprec.value = 1e-5
- self._activate_estimate_supercells()
- return
-
- def get_panel_value(self):
- """Return a dictionary with the input parameters for the plugin."""
- return {
- "simulation_mode": self.calc_options.value,
- "supercell_selector": self.supercell,
- "symmetry_symprec": self.symmetry_symprec.value,
- }
-
- def set_panel_value(self, input_dict):
- """Load a dictionary with the input parameters for the plugin."""
- self.calc_options.value = input_dict.get("simulation_mode", 1)
- self.supercell = input_dict.get("supercell_selector", [2, 2, 2])
- self.symmetry_symprec.value = input_dict.get("symmetry_symprec", 1e-5)
- self._sc_x.value, self._sc_y.value, self._sc_z.value = self.supercell
-
- def reset(self):
- """Reset the panel"""
- self.calc_options.value = 1
- self.supercell = [2, 2, 2]
- self.symmetry_symprec.value = 1e-5
- self._sc_x.value, self._sc_y.value, self._sc_z.value = self.supercell
+ def _on_change_simulation_type(self, _):
+ self.supercell_widget.layout.display = (
+ "block" if self._model.simulation_type in [1, 3] else "none"
+ )
+ self.supercell_number_estimator.layout.display = (
+ "block"
+ if self._model.simulation_type in [1, 3]
+ and len(self._model.input_structure.sites) <= 30
+ else "none"
+ )
+ self.supercell_estimate_button.layout.display = (
+ "block"
+ if self._model.simulation_type in [1, 3]
+ and len(self._model.input_structure.sites) <= 30
+ else "none"
+ )
diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/dielectricmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/dielectricmodel.py
new file mode 100644
index 0000000..95f376f
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/dielectricmodel.py
@@ -0,0 +1,162 @@
+from aiidalab_qe.common.mvc import Model
+import traitlets as tl
+from aiida.common.extendeddicts import AttributeDict
+from aiidalab_qe_vibroscopy.utils.dielectric.result import NumpyEncoder
+import numpy as np
+import base64
+import json
+from IPython.display import display
+from aiidalab_qe_vibroscopy.utils.dielectric.result import export_dielectric_data
+
+
+class DielectricModel(Model):
+ vibro = tl.Instance(AttributeDict, allow_none=True)
+
+ site_selector_options = tl.List(
+ trait=tl.Tuple((tl.Unicode(), tl.Int())),
+ )
+ dielectric_data = {}
+
+ dielectric_tensor_table = tl.Unicode("")
+ born_charges_table = tl.Unicode("")
+ raman_tensors_table = tl.Unicode("")
+ site = tl.Int()
+
+ def fetch_data(self):
+ """Fetch the dielectric data from the VibroWorkChain"""
+ self.dielectric_data = export_dielectric_data(self.vibro)
+
+ 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"""
+
+
+ """
+ return table_html
diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/dielectricwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/dielectricwidget.py
new file mode 100644
index 0000000..ac6c889
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/dielectricwidget.py
@@ -0,0 +1,110 @@
+import ipywidgets as ipw
+from aiidalab_qe_vibroscopy.app.widgets.dielectricmodel import DielectricModel
+from aiidalab_qe.common.widgets import LoadingWidget
+
+
+class DielectricWidget(ipw.VBox):
+ """
+ Widget for displaying dielectric properties results
+ """
+
+ def __init__(self, model: DielectricModel, node: None, **kwargs):
+ super().__init__(
+ children=[LoadingWidget("Loading widgets")],
+ **kwargs,
+ )
+ self._model = model
+
+ self.rendered = False
+ self._model.vibro = node
+
+ def render(self):
+ if self.rendered:
+ return
+
+ 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.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.fetch_data()
+ 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/app/widgets/euphonic/model.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/model.py
new file mode 100644
index 0000000..264ca43
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/model.py
@@ -0,0 +1,15 @@
+from aiidalab_qe.common.panel import ResultsModel
+from aiida.common.extendeddicts import AttributeDict
+import traitlets as tl
+from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import (
+ export_euphonic_data,
+)
+
+
+class EuphonicModel(ResultsModel):
+ node = tl.Instance(AttributeDict, allow_none=True)
+
+ def fetch_data(self):
+ ins_data = export_euphonic_data(self.node)
+ self.fc = ins_data["fc"]
+ self.q_path = ins_data["q_path"]
diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/powder_full_model.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/powder_full_model.py
new file mode 100644
index 0000000..1f1a1a3
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/powder_full_model.py
@@ -0,0 +1,8 @@
+from __future__ import annotations
+from aiidalab_qe.common.mvc import Model
+import traitlets as tl
+from aiida.common.extendeddicts import AttributeDict
+
+
+class PowderFullModel(Model):
+ vibro = tl.Instance(AttributeDict, allow_none=True)
diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/powder_full_widget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/powder_full_widget.py
new file mode 100644
index 0000000..839fe69
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/powder_full_widget.py
@@ -0,0 +1,40 @@
+import ipywidgets as ipw
+from aiidalab_qe_vibroscopy.app.widgets.euphonic.powder_full_model import (
+ PowderFullModel,
+)
+
+
+class PowderFullWidget(ipw.VBox):
+ def __init__(self, model: PowderFullModel, node: None, **kwargs):
+ super().__init__(
+ children=[ipw.HTML("Loading Powder data...")],
+ **kwargs,
+ )
+ self._model = model
+ self._model.vibro = node
+ self.rendered = False
+
+ def render(self):
+ if self.rendered:
+ return
+
+ self.children = [ipw.HTML("Here goes widgets for Powder data")]
+
+ self.rendered = True
+
+ # self._model.fetch_data()
+ # self._needs_powder_widget()
+ # self.render_widgets()
+
+ # def _needs_powder_widget(self):
+ # if self._model.needs_powder_tab:
+ # self.powder_model = PowderModel()
+ # self.powder_widget = PowderWidget(
+ # model=self.powder_model,
+ # node=self._model.vibro,
+ # )
+ # self.children = (*self.children, self.powder_widget)
+
+ # def render_widgets(self):
+ # if self._model.needs_powder_tab:
+ # self.powder_widget.render()
diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py
new file mode 100644
index 0000000..308aedb
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py
@@ -0,0 +1,157 @@
+from __future__ import annotations
+
+from aiidalab_qe.common.mvc import Model
+import traitlets as tl
+from aiida.common.extendeddicts import AttributeDict
+from IPython.display import display
+from euphonic import ForceConstants
+
+
+class SingleCrystalFullModel(Model):
+ node = tl.Instance(AttributeDict, allow_none=True)
+
+ fc = tl.Instance(ForceConstants, allow_none=True)
+ q_path = tl.Dict(allow_none=True)
+
+ custom_kpath = tl.Unicode("")
+ q_spacing = tl.Float(0.01)
+ energy_broadening = tl.Float(0.05)
+ energy_bins = tl.Int(200)
+ temperature = tl.Float(0)
+ weighting = tl.Unicode("coherent")
+
+ E_units_button_options = tl.List(
+ trait=tl.List(tl.Unicode()),
+ default_value=[
+ ("meV", "meV"),
+ ("THz", "THz"),
+ ],
+ )
+ E_units = tl.Unicode("meV")
+
+ slider_intensity = tl.List(
+ trait=tl.Float(),
+ default_value=[1, 10],
+ )
+
+ parameters = tl.Dict()
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.update_parameters()
+
+ # Observe changes in dependent trailets
+ self.observe(
+ self.update_parameters,
+ names=[
+ "weighting",
+ "E_units",
+ "temperature",
+ "q_spacing",
+ "energy_broadening",
+ "energy_bins",
+ ],
+ )
+
+ def update_parameters(self):
+ """Update the parameters dictionary dynamically."""
+ self.parameters = {
+ "weighting": self.weighting,
+ "grid": None,
+ "grid_spacing": 0.1,
+ "energy_units": self.E_units,
+ "temperature": self.temperature,
+ "shape": "gauss",
+ "length_unit": "angstrom",
+ "q_spacing": self.q_spacing,
+ "energy_broadening": self.energy_broadening,
+ "q_broadening": None,
+ "ebins": self.energy_bins,
+ "e_min": 0,
+ "e_max": None,
+ "title": None,
+ "ylabel": "THz",
+ "xlabel": "",
+ "save_json": False,
+ "no_base_style": False,
+ "style": False,
+ "vmin": None,
+ "vmax": None,
+ "save_to": None,
+ "asr": None,
+ "dipole_parameter": 1.0,
+ "use_c": None,
+ "n_threads": None,
+ }
+
+ def _update_spectra(self):
+ q_path = self.q_path
+ if self.custom_kpath:
+ q_path = self.q_path
+ q_path["coordinates"], q_path["labels"] = self.curate_path_and_labels(
+ self.custom_kpath
+ )
+ q_path["delta_q"] = self.q_spacing
+
+ # spectra, parameters = produce_bands_weigthed_data(
+ # params=self.parameters,
+ # fc=self.fc,
+ # linear_path=q_path,
+ # plot=False,
+ # )
+
+ # if self.custom_path:
+ # self.x, self.y = np.meshgrid(
+ # spectra[0].x_data.magnitude, spectra[0].y_data.magnitude
+ # )
+ # (
+ # self.final_xspectra,
+ # self.final_zspectra,
+ # self.ticks_positions,
+ # self.ticks_labels,
+ # ) = generated_curated_data(spectra)
+ # else:
+ # # Spectrum2D as output of the powder data
+ # self.x, self.y = np.meshgrid(
+ # spectra.x_data.magnitude, spectra.y_data.magnitude
+ # )
+
+ # # we don't need to curate the powder data,
+ # # we can directly use them:
+ # self.final_xspectra = spectra.x_data.magnitude
+ # self.final_zspectra = spectra.z_data.magnitude
+
+ def curate_path_and_labels(self, path):
+ # This is used to curate the path and labels of the spectra if custom kpath is provided.
+ # I do not like this implementation (MB)
+ coordinates = []
+ labels = []
+ linear_paths = path.split("|")
+ for i in linear_paths:
+ scoords = []
+ s = i.split(
+ " - "
+ ) # not i.split("-"), otherwise also the minus of the negative numbers are used for the splitting.
+ for k in s:
+ labels.append(k.strip())
+ # AAA missing support for fractions.
+ l = tuple(map(float, [kk for kk in k.strip().split(" ")])) # noqa: E741
+ scoords.append(l)
+ coordinates.append(scoords)
+ return coordinates, labels
+
+ @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/euphonic/single_crystal_widget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_widget.py
new file mode 100644
index 0000000..37d313e
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_widget.py
@@ -0,0 +1,218 @@
+import ipywidgets as ipw
+from aiidalab_qe_vibroscopy.app.widgets.euphonic.single_crystal_model import (
+ SingleCrystalFullModel,
+)
+import plotly.graph_objects as go
+
+
+class SingleCrystalFullWidget(ipw.VBox):
+ def __init__(self, model: SingleCrystalFullModel, node: None, **kwargs):
+ super().__init__(
+ children=[ipw.HTML("Loading Single Crystal data...")],
+ **kwargs,
+ )
+ self._model = model
+ self._model.vibro = node
+ self.rendered = False
+
+ def render(self):
+ if self.rendered:
+ return
+
+ self.custom_kpath_description = ipw.HTML(
+ """
+
+ Custom q-points path for the structure factor:
+ we can provide it via a specific format:
+ (1) each linear path should be divided by '|';
+ (2) each path is composed of 'qxi qyi qzi - qxf qyf qzf' where qxi and qxf are, respectively,
+ the start and end x-coordinate of the q direction, in reciprocal lattice units (rlu).
+ An example path is: '0 0 0 - 1 1 1 | 1 1 1 - 0.5 0.5 0.5'.
+ For now, we do not support fractions (i.e. we accept 0.5 but not 1/2).
+
+ """
+ )
+
+ self.fig = go.FigureWidget()
+
+ self.slider_intensity = ipw.FloatRangeSlider(
+ min=1,
+ max=100,
+ step=1,
+ orientation="horizontal",
+ readout=True,
+ readout_format=".0f",
+ layout=ipw.Layout(
+ width="400px",
+ ),
+ )
+ ipw.link(
+ (self._model, "slider_intensity"),
+ (self.slider_intensity, "value"),
+ )
+
+ self.E_units_button = ipw.ToggleButtons(
+ description="Energy units:",
+ layout=ipw.Layout(
+ width="auto",
+ ),
+ )
+ ipw.dlink(
+ (self._model, "E_units_button_options"),
+ (self.E_units_button, "options"),
+ )
+
+ ipw.link(
+ (self._model, "E_units"),
+ (self.E_units_button, "value"),
+ )
+
+ self.plot_button = ipw.Button(
+ description="Replot",
+ icon="pencil",
+ button_style="primary",
+ disabled=True,
+ layout=ipw.Layout(width="auto"),
+ )
+
+ self.reset_button = ipw.Button(
+ description="Reset",
+ icon="recycle",
+ button_style="primary",
+ disabled=False,
+ layout=ipw.Layout(width="auto"),
+ )
+
+ self.download_button = ipw.Button(
+ description="Download Data and Plot",
+ icon="download",
+ button_style="primary",
+ disabled=False, # Large files...
+ layout=ipw.Layout(width="auto"),
+ )
+
+ self.q_spacing = ipw.FloatText(
+ step=0.001,
+ description="q step (1/A)",
+ tooltip="q spacing in 1/A",
+ )
+ ipw.link(
+ (self._model, "q_spacing"),
+ (self.q_spacing, "value"),
+ )
+ self.energy_broadening = ipw.FloatText(
+ step=0.01,
+ description="ΔE (meV)",
+ tooltip="Energy broadening in meV",
+ )
+ ipw.link(
+ (self._model, "energy_broadening"),
+ (self.energy_broadening, "value"),
+ )
+
+ self.energy_bins = ipw.IntText(
+ description="#E bins",
+ tooltip="Number of energy bins",
+ )
+ ipw.link(
+ (self._model, "energy_bins"),
+ (self.energy_bins, "value"),
+ )
+
+ self.temperature = ipw.FloatText(
+ step=0.01,
+ description="T (K)",
+ disabled=False,
+ )
+ ipw.link(
+ (self._model, "temperature"),
+ (self.temperature, "value"),
+ )
+
+ self.weight_button = ipw.ToggleButtons(
+ options=[
+ ("Coherent", "coherent"),
+ ("DOS", "dos"),
+ ],
+ description="weight:",
+ disabled=False,
+ style={"description_width": "initial"},
+ )
+ ipw.link(
+ (self._model, "weighting"),
+ (self.weight_button, "value"),
+ )
+
+ self.custom_kpath = ipw.Text(
+ description="Custom path (rlu):",
+ style={"description_width": "initial"},
+ )
+
+ ipw.link(
+ (self._model, "custom_kpath"),
+ (self.custom_kpath, "value"),
+ )
+
+ self.children = [
+ ipw.HTML("Neutron dynamic structure factor - Single Crystal
"),
+ self.fig,
+ self.slider_intensity,
+ ipw.HTML("(Intensity is relative to the maximum intensity at T=0K)"),
+ self.E_units_button,
+ ipw.HBox(
+ [
+ ipw.VBox(
+ [
+ ipw.HBox(
+ [
+ self.reset_button,
+ self.plot_button,
+ self.download_button,
+ ]
+ ),
+ self.q_spacing,
+ self.energy_broadening,
+ self.energy_bins,
+ self.temperature,
+ self.weight_button,
+ ],
+ layout=ipw.Layout(
+ width="60%",
+ ),
+ ),
+ ipw.VBox(
+ [
+ self.custom_kpath_description,
+ self.custom_kpath,
+ ],
+ layout=ipw.Layout(
+ width="70%",
+ ),
+ ),
+ ], # end of HBox children
+ ),
+ ]
+
+ self.rendered = True
+ self._init_view()
+
+ def _init_view(self, _=None):
+ print("Init view")
+ # self._model._update_spectra()
+
+ # self._model.fetch_data()
+ # self._needs_single_crystal_widget()
+ # self.render_widgets()
+
+ # def _needs_single_crystal_widget(self):
+ # if self._model.needs_single_crystal_tab:
+ # self.single_crystal_model = SingleCrystalModel()
+ # self.single_crystal_widget = SingleCrystalWidget(
+ # model=self.single_crystal_model,
+ # node=self._model.vibro,
+ # )
+ # self.children = (*self.children, self.single_crystal_widget)
+
+ # def render_widgets(self):
+ # if self._model.needs_single_crystal_tab:
+ # self.single_crystal_widget.render()
diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/widget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/widget.py
new file mode 100644
index 0000000..68ece6c
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/widget.py
@@ -0,0 +1,98 @@
+import ipywidgets as ipw
+from aiidalab_qe_vibroscopy.app.widgets.euphonic.model import EuphonicModel
+from aiidalab_qe_vibroscopy.app.widgets.euphonic.single_crystal_widget import (
+ SingleCrystalFullWidget,
+)
+from aiidalab_qe_vibroscopy.app.widgets.euphonic.single_crystal_model import (
+ SingleCrystalFullModel,
+)
+from aiidalab_qe_vibroscopy.app.widgets.euphonic.powder_full_widget import (
+ PowderFullWidget,
+)
+from aiidalab_qe_vibroscopy.app.widgets.euphonic.powder_full_model import (
+ PowderFullModel,
+)
+
+
+class EuphonicWidget(ipw.VBox):
+ """
+ Widget for the Euphonic Results
+ """
+
+ def __init__(self, model: EuphonicModel, node: None, **kwargs):
+ super().__init__(children=[ipw.HTML("Loading Euphonic data...")], **kwargs)
+ self._model = model
+ self._model.node = node
+ self.rendered = False
+
+ def render(self):
+ if self.rendered:
+ return
+
+ self.rendering_results_button = ipw.Button(
+ description="Initialise INS data",
+ icon="pencil",
+ button_style="primary",
+ layout=ipw.Layout(width="auto"),
+ )
+ self.rendering_results_button.on_click(
+ self._on_rendering_results_button_clicked
+ )
+
+ self.tabs = ipw.Tab(
+ layout=ipw.Layout(min_height="250px"),
+ selected_index=None,
+ )
+ self.tabs.observe(
+ self._on_tab_change,
+ "selected_index",
+ )
+
+ self.children = [
+ ipw.HBox(
+ [
+ ipw.HTML("Click the button to initialise the INS data."),
+ self.rendering_results_button,
+ ]
+ )
+ ]
+
+ self.rendered = True
+ self._model.fetch_data()
+ # self.render_widgets()
+
+ def _on_rendering_results_button_clicked(self, _):
+ self.children = []
+ tab_data = []
+
+ single_crystal_model = SingleCrystalFullModel()
+ single_crystal_widget = SingleCrystalFullWidget(
+ model=single_crystal_model,
+ node=self._model.node,
+ )
+ # We need to link the q_path and fc from the EuphonicModel to the SingleCrystalModel
+ single_crystal_widget._model.q_path = self._model.q_path
+ single_crystal_widget._model.fc = self._model.fc
+
+ powder_model = PowderFullModel()
+ powder_widget = PowderFullWidget(
+ model=powder_model,
+ node=self._model.node,
+ )
+ qplane_widget = ipw.HTML("Q-plane view data")
+ tab_data.append(("Single crystal", single_crystal_widget))
+ tab_data.append(("Powder", powder_widget))
+ tab_data.append(("Q-plane view", qplane_widget))
+ # Assign children and titles dynamically
+ self.tabs.children = [content for _, content in tab_data]
+
+ for index, (title, _) in enumerate(tab_data):
+ self.tabs.set_title(index, title)
+
+ self.children = [self.tabs]
+ self.tabs.selected_index = 0
+
+ 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/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py
new file mode 100644
index 0000000..687b838
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py
@@ -0,0 +1,252 @@
+import numpy as np
+import traitlets as tl
+import copy
+
+from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import (
+ AttrDict,
+ produce_bands_weigthed_data,
+ produce_powder_data,
+ generated_curated_data,
+ par_dict,
+ par_dict_powder,
+ export_euphonic_data,
+ generate_force_constant_instance,
+)
+
+from aiidalab_qe_vibroscopy.utils.euphonic.tab_widgets.euphonic_q_planes_widgets import (
+ produce_Q_section_modes,
+ produce_Q_section_spectrum,
+)
+
+from aiidalab_qe.common.mvc import Model
+
+
+class EuphonicBaseResultsModel(Model):
+ """Model for the neutron scattering results panel."""
+
+ # Here we mode all the model and data-controller, i.e. all the data and their
+ # manipulation to produce new spectra.
+ # plot-controller, i.e. scales, colors and so on, should be attached to the widget, I think.
+ # the above last point should be discussed more.
+
+ # For now, we do only the first of the following:
+ # 1. single crystal data: sc
+ # 2. powder average: pa
+ # 3. Q planes: qp
+ # TRY it on the detached app.
+
+ # AAA TOBE defined with respect to the type
+ spectra = {}
+ path = []
+ q_path = None
+
+ # Settings for single crystal and powder average
+ q_spacing = tl.Float(0.01)
+ energy_broadening = tl.Float(0.05)
+ energy_bins = tl.Int(200)
+ temperature = tl.Float(0)
+ weighting = tl.Unicode("coherent")
+
+ def fetch_data(self):
+ """Fetch the data from the database or from the uploaded files."""
+ # 1. from aiida, so we have the node
+ if self.node:
+ ins_data = export_euphonic_data(self.node)
+ self.fc = ins_data["fc"]
+ self.q_path = ins_data["q_path"]
+ # 2. from uploaded files...
+ else:
+ self.fc = self.upload_widget._read_phonopy_files(
+ fname=self.fname,
+ phonopy_yaml_content=self._model.phonopy_yaml_content,
+ fc_hdf5_content=self._model.fc_hdf5_content,
+ )
+
+ def _inject_single_crystal_settings(
+ self,
+ ):
+ self.parameters = copy.deepcopy(
+ par_dict
+ ) # need to be different if powder or q section.
+ self._callback_spectra_generation = produce_bands_weigthed_data
+ # Dynamically add a trait for single crystal settings
+ self.add_traits(custom_kpath=tl.Unicode(""))
+
+ def _inject_powder_settings(
+ self,
+ ):
+ self.parameters = copy.deepcopy(par_dict_powder)
+ self._callback_spectra_generation = produce_powder_data
+ # Dynamically add a trait for powder settings
+ self.add_traits(q_min=tl.Float(0.0))
+ self.add_traits(q_max=tl.Float(1))
+ self.add_traits(npts=tl.Int(100))
+
+ def _inject_qsection_settings(
+ self,
+ ):
+ # self._callback_spectra_generation = produce_Q_section_modes
+ # Dynamically add a trait for q section settings
+ self.add_traits(center_e=tl.Float(0.0))
+ self.add_traits(Q0_vec=tl.List(trait=tl.Float(), default_value=[0.0, 0.0, 0.0]))
+ self.add_traits(
+ h_vec=tl.List(trait=tl.Float(), default_value=[1, 1, 1, 100, 1])
+ )
+ self.add_traits(
+ k_vec=tl.List(trait=tl.Float(), default_value=[1, 1, 1, 100, 1])
+ )
+
+ def set_model_state(self, parameters: dict):
+ for k, v in parameters.items():
+ setattr(self, k, v)
+
+ def _get_default(self, trait):
+ if trait in ["h_vec", "k_vec"]:
+ return [1, 1, 1, 100, 1]
+ elif trait == "Q0_vec":
+ return [0.0, 0.0, 0.0]
+ return self.traits()[trait].default_value
+
+ def get_model_state(self):
+ return {trait: getattr(self, trait) for trait in self.traits()}
+
+ def reset(
+ self,
+ ):
+ with self.hold_trait_notifications():
+ for trait in self.traits():
+ setattr(self, trait, self._get_default(trait))
+
+ def _update_spectra(
+ self,
+ ):
+ # This is used to update the spectra when the parameters are changed
+ # and the
+ if not hasattr(self, "parameters"):
+ self._inject_single_crystal_settings()
+
+ self.parameters.update(self.get_model_state())
+ parameters_ = AttrDict(self.parameters)
+
+ # custom linear path
+ custom_kpath = self.custom_kpath if hasattr(self, "custom_kpath") else ""
+ if len(custom_kpath) > 1:
+ coordinates, labels = self.curate_path_and_labels()
+ qpath = {
+ "coordinates": coordinates,
+ "labels": labels, # ["$\Gamma$","X","X","(1,1,1)"],
+ "delta_q": parameters_["q_spacing"],
+ }
+ else:
+ qpath = copy.deepcopy(self.q_path)
+ if qpath:
+ qpath["delta_q"] = parameters_["q_spacing"]
+
+ spectra, parameters = self._callback_spectra_generation(
+ params=parameters_,
+ fc=self.fc,
+ linear_path=qpath,
+ plot=False,
+ )
+
+ # curated spectra (labels and so on...)
+ if hasattr(self, "custom_kpath"): # single crystal case
+ self.x, self.y = np.meshgrid(
+ spectra[0].x_data.magnitude, spectra[0].y_data.magnitude
+ )
+ (
+ self.final_xspectra,
+ self.final_zspectra,
+ self.ticks_positions,
+ self.ticks_labels,
+ ) = generated_curated_data(spectra)
+ else:
+ # Spectrum2D as output of the powder data
+ self.x, self.y = np.meshgrid(
+ spectra.x_data.magnitude, spectra.y_data.magnitude
+ )
+
+ # we don't need to curate the powder data,
+ # we can directly use them:
+ self.final_xspectra = spectra.x_data.magnitude
+ self.final_zspectra = spectra.z_data.magnitude
+
+ def _update_qsection_spectra(
+ self,
+ ):
+ parameters_ = AttrDict(
+ {
+ "h": np.array([i for i in self.h_vec[:-2]]),
+ "k": np.array([i for i in self.k_vec[:-2]]),
+ "n_h": int(self.h_vec[-2]),
+ "n_k": int(self.k_vec[-2]),
+ "h_extension": self.h_vec[-1],
+ "k_extension": self.k_vec[-1],
+ "Q0": np.array([i for i in self.Q0_vec[:]]),
+ "ecenter": self.center_e,
+ "deltaE": self.energy_broadening,
+ "bins": self.energy_bins,
+ "spectrum_type": self.weighting,
+ "temperature": self.temperature,
+ }
+ )
+
+ modes, q_array, h_array, k_array, labels, dw = produce_Q_section_modes(
+ self.fc,
+ h=parameters_.h,
+ k=parameters_.k,
+ Q0=parameters_.Q0,
+ n_h=parameters_.n_h,
+ n_k=parameters_.n_k,
+ h_extension=parameters_.h_extension,
+ k_extension=parameters_.k_extension,
+ temperature=parameters_.temperature,
+ )
+
+ self.av_spec, self.q_array, self.h_array, self.k_array, self.labels = (
+ produce_Q_section_spectrum(
+ modes,
+ q_array,
+ h_array,
+ k_array,
+ ecenter=parameters_.ecenter,
+ deltaE=parameters_.deltaE,
+ bins=parameters_.bins,
+ spectrum_type=parameters_.spectrum_type,
+ dw=dw,
+ labels=labels,
+ )
+ )
+
+ def curate_path_and_labels(
+ self,
+ ):
+ # This is used to curate the path and labels of the spectra if custom kpath is provided.
+ # I do not like this implementation (MB)
+ coordinates = []
+ labels = []
+ path = self.custom_kpath
+ linear_paths = path.split("|")
+ for i in linear_paths:
+ scoords = []
+ s = i.split(
+ " - "
+ ) # not i.split("-"), otherwise also the minus of the negative numbers are used for the splitting.
+ for k in s:
+ labels.append(k.strip())
+ # AAA missing support for fractions.
+ l = tuple(map(float, [kk for kk in k.strip().split(" ")])) # noqa: E741
+ scoords.append(l)
+ coordinates.append(scoords)
+ return coordinates, labels
+
+ def _clone(self):
+ return copy.deepcopy(self)
+
+ def produce_phonopy_files(self):
+ # This is used to produce the phonopy files from
+ # PhonopyCalculation data. The files are phonopy.yaml and force_constants.hdf5
+ phonopy_yaml, fc_hdf5 = generate_force_constant_instance(
+ self.node.phonon_bands.creator, mode="download"
+ )
+ return phonopy_yaml, fc_hdf5
diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py
new file mode 100644
index 0000000..2a3ad09
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py
@@ -0,0 +1,312 @@
+import pathlib
+import tempfile
+
+
+from IPython.display import display
+
+import ipywidgets as ipw
+
+# from ..euphonic.bands_pdos import *
+from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import (
+ generate_force_constant_instance,
+ export_euphonic_data, # noqa: F401
+)
+from aiidalab_qe_vibroscopy.utils.euphonic.tab_widgets.euphonic_single_crystal_widgets import (
+ SingleCrystalFullWidget,
+)
+from aiidalab_qe_vibroscopy.utils.euphonic.tab_widgets.euphonic_powder_widgets import (
+ PowderFullWidget,
+)
+from aiidalab_qe_vibroscopy.utils.euphonic.tab_widgets.euphonic_q_planes_widgets import (
+ QSectionFullWidget,
+)
+
+
+from aiidalab_qe.common.widgets import LoadingWidget
+###### START for detached app:
+
+
+# Upload buttons
+class UploadPhonopyYamlWidget(ipw.FileUpload):
+ def __init__(self, **kwargs):
+ super().__init__(
+ description="upload phonopy YAML file",
+ multiple=False,
+ layout={"width": "initial"},
+ )
+
+
+class UploadForceConstantsHdf5Widget(ipw.FileUpload):
+ def __init__(self, **kwargs):
+ super().__init__(
+ description="upload force constants HDF5 file",
+ multiple=False,
+ layout={"width": "initial"},
+ )
+
+
+class UploadPhonopyWidget(ipw.HBox):
+ def __init__(self, **kwargs):
+ self.upload_phonopy_yaml = UploadPhonopyYamlWidget(**kwargs)
+ self.upload_phonopy_hdf5 = UploadForceConstantsHdf5Widget(**kwargs)
+
+ self.reset_uploads = ipw.Button(
+ description="Discard uploaded files",
+ icon="pencil",
+ button_style="warning",
+ disabled=False,
+ layout=ipw.Layout(width="auto"),
+ )
+
+ super().__init__(
+ children=[
+ self.upload_phonopy_yaml,
+ self.upload_phonopy_hdf5,
+ self.reset_uploads,
+ ],
+ **kwargs,
+ )
+
+ def _read_phonopy_files(self, fname, phonopy_yaml_content, fc_hdf5_content=None):
+ suffix = "".join(pathlib.Path(fname).suffixes)
+
+ with tempfile.NamedTemporaryFile(suffix=suffix) as temp_yaml:
+ temp_yaml.write(phonopy_yaml_content)
+ temp_yaml.flush()
+
+ if fc_hdf5_content:
+ with tempfile.NamedTemporaryFile(suffix=".hdf5") as temp_file:
+ temp_file.write(fc_hdf5_content)
+ temp_file.flush()
+ temp_hdf5_name = temp_file.name
+
+ try:
+ fc = generate_force_constant_instance(
+ path=pathlib.Path(fname),
+ summary_name=temp_yaml.name,
+ fc_name=temp_hdf5_name,
+ )
+ except ValueError:
+ return None
+
+ return fc
+ else:
+ temp_hdf5_name = None
+
+ try:
+ fc = generate_force_constant_instance(
+ path=pathlib.Path(fname),
+ summary_name=temp_yaml.name,
+ # fc_name=temp_hdf5_name,
+ )
+ except ValueError:
+ return None
+
+ return fc
+
+
+#### END for detached app
+
+
+##### START OVERALL WIDGET TO DISPLAY EVERYTHING:
+
+
+class EuphonicSuperWidget(ipw.VBox):
+ """
+ Widget that will include everything,
+ from the upload widget to the tabs with single crystal and powder predictions.
+ In between, we trigger the initialization of plots via a button.
+ """
+
+ def __init__(
+ self, mode="aiidalab-qe app plugin", model=None, node=None, fc=None, q_path=None
+ ):
+ """
+ Initialize the Euphonic utility class.
+ Parameters:
+ -----------
+ mode : str, optional
+ The mode of operation, default is "aiidalab-qe app plugin".
+ fc : optional
+ Force constants, default is None.
+ q_path : optional
+ Q-path for phonon calculations, default is None. If Low-D system, this can be provided.
+ It is the same path obtained for the PhonopyCalculation of the phonopy_bands.
+ Attributes:
+ -----------
+ mode : str
+ The mode of operation.
+ upload_widget : UploadPhonopyWidget
+ Widget for uploading phonopy files.
+ fc_hdf5_content : None
+ Content of the force constants HDF5 file.
+ tab_widget : ipw.Tab
+ Tab widget for different views.
+ plot_button : ipw.Button
+ Button to initialize INS data.
+ fc : optional
+ Force constants if provided.
+ """
+
+ self.mode = mode
+ self._model = model # this is the single crystal model.
+ self._model.node = node
+ self._model.fc_hdf5_content = None
+
+ self.rendered = False
+
+ super().__init__()
+
+ def render(self):
+ if self.rendered:
+ return
+
+ self.upload_widget = UploadPhonopyWidget()
+ self.upload_widget.reset_uploads.on_click(self._on_reset_uploads_button_clicked)
+
+ self.tab_widget = ipw.Tab()
+ self.tab_widget.layout.display = "none"
+ self.tab_widget.set_title(0, "Single crystal")
+ self.tab_widget.set_title(1, "Powder sample")
+ self.tab_widget.set_title(2, "Q-plane view")
+ self.tab_widget.children = ()
+
+ self.plot_button = ipw.Button(
+ description="Initialise INS data",
+ icon="pencil",
+ button_style="primary",
+ disabled=True,
+ layout=ipw.Layout(width="auto"),
+ )
+ self.plot_button.on_click(self._on_first_plot_button_clicked)
+
+ self.loading_widget = LoadingWidget("Loading INS data")
+ self.loading_widget.layout.display = "none"
+
+ if self.mode == "aiidalab-qe app plugin":
+ self.upload_widget.layout.display = "none"
+ self.plot_button.disabled = False
+ else:
+ self.upload_widget.children[0].observe(self._on_upload_yaml, "_counter")
+ self.upload_widget.children[1].observe(self._on_upload_hdf5, "_counter")
+
+ self.download_widget = DownloadYamlHdf5Widget(model=self._model)
+ self.download_widget.layout.display = "none"
+
+ self.children = [
+ self.upload_widget,
+ self.plot_button,
+ self.loading_widget,
+ self.tab_widget,
+ self.download_widget,
+ ]
+
+ self.rendered = True
+
+ def _on_reset_uploads_button_clicked(self, change):
+ self.upload_widget.upload_phonopy_yaml.value.clear()
+ self.upload_widget.upload_phonopy_yaml._counter = 0
+ self.upload_widget.upload_phonopy_hdf5.value.clear()
+ self.upload_widget.upload_phonopy_hdf5._counter = 0
+
+ self.plot_button.layout.display = "block"
+ self.plot_button.disabled = True
+
+ self.tab_widget.children = ()
+
+ self.tab_widget.layout.display = "none"
+
+ def _on_upload_yaml(self, change):
+ if change["new"] != change["old"]:
+ for fname in self.upload_widget.children[
+ 0
+ ].value.keys(): # always one key because I allow only one file at the time.
+ self.fname = fname
+ self._model.phonopy_yaml_content = self.upload_widget.children[0].value[
+ fname
+ ]["content"]
+
+ if self.plot_button.disabled:
+ self.plot_button.disabled = False
+
+ def _on_upload_hdf5(self, change):
+ if change["new"] != change["old"]:
+ for fname in self.upload_widget.children[1].value.keys():
+ self._model.fc_hdf5_content = self.upload_widget.children[1].value[
+ fname
+ ]["content"]
+
+ def _on_first_plot_button_clicked(self, change=None): # basically the render.
+ # It creates the widgets
+ self.plot_button.layout.display = "none"
+ self.loading_widget.layout.display = "block"
+
+ self._model.fetch_data() # should be in the model.
+ powder_model = self._model._clone()
+ qsection_model = self._model._clone()
+
+ # I first initialise this widget, to then have the 0K ref for the other two.
+ # the model is passed to the widget. For the other two, I need to generate the model.
+ singlecrystalwidget = SingleCrystalFullWidget(model=self._model)
+
+ # I need to generate the models for the other two widgets.
+ self._model._inject_single_crystal_settings()
+ powder_model._inject_powder_settings()
+ qsection_model._inject_qsection_settings()
+
+ self.tab_widget.children = (
+ singlecrystalwidget,
+ PowderFullWidget(model=powder_model),
+ QSectionFullWidget(model=qsection_model),
+ )
+
+ for widget in self.tab_widget.children:
+ widget.render() # this is the render method of the widget.
+
+ self.loading_widget.layout.display = "none"
+ self.tab_widget.layout.display = "block"
+ self.download_widget.layout.display = "block"
+
+
+class DownloadYamlHdf5Widget(ipw.HBox):
+ def __init__(self, model):
+ self._model = model
+
+ self.download_button = ipw.Button(
+ description="Download phonopy data",
+ icon="pencil",
+ button_style="primary",
+ disabled=False,
+ layout=ipw.Layout(width="auto"),
+ )
+ self.download_button.on_click(self._download_data)
+
+ super().__init__(
+ children=[
+ self.download_button,
+ ],
+ )
+
+ def _download_data(self, _=None):
+ """
+ Download both the phonopy.yaml and fc.hdf5 files.
+ """
+ phonopy_yaml, fc_hdf5 = self._model.produce_phonopy_files()
+ self._download(payload=phonopy_yaml, filename="phonopy" + ".yaml")
+ self._download(payload=fc_hdf5, filename="fc" + ".hdf5")
+
+ @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/ir_ramanmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/ir_ramanmodel.py
new file mode 100644
index 0000000..8d9e604
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/ir_ramanmodel.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+from aiidalab_qe.common.mvc import Model
+from aiida.common.extendeddicts import AttributeDict
+from ase.atoms import Atoms
+import traitlets as tl
+
+from aiidalab_qe_vibroscopy.utils.raman.result import export_iramanworkchain_data
+
+
+class IRRamanModel(Model):
+ vibro = tl.Instance(AttributeDict, allow_none=True)
+ input_structure = tl.Instance(Atoms, allow_none=True)
+
+ needs_raman_tab = tl.Bool()
+ needs_ir_tab = tl.Bool()
+
+ def fetch_data(self):
+ spectra_data = export_iramanworkchain_data(self.vibro)
+ if spectra_data["Ir"] == "No IR modes detected.":
+ self.needs_ir_tab = False
+ else:
+ self.needs_ir_tab = True
+ if spectra_data["Raman"] == "No Raman modes detected.":
+ self.needs_raman_tab = False
+ else:
+ self.needs_raman_tab = True
diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/ir_ramanwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/ir_ramanwidget.py
new file mode 100644
index 0000000..3666076
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/ir_ramanwidget.py
@@ -0,0 +1,58 @@
+from aiidalab_qe_vibroscopy.app.widgets.ir_ramanmodel import IRRamanModel
+from aiidalab_qe_vibroscopy.app.widgets.ramanwidget import RamanWidget
+from aiidalab_qe_vibroscopy.app.widgets.ramanmodel import RamanModel
+import ipywidgets as ipw
+
+
+class IRRamanWidget(ipw.VBox):
+ def __init__(self, model: IRRamanModel, node: None, input_structure, **kwargs):
+ super().__init__(
+ children=[ipw.HTML("Loading Raman data...")],
+ **kwargs,
+ )
+ self._model = model
+ self._model.vibro = node
+ self._model.input_structure = input_structure
+ self.rendered = False
+
+ def render(self):
+ if self.rendered:
+ return
+
+ self.children = []
+
+ self.rendered = True
+ self._model.fetch_data()
+ self._needs_raman_widget()
+ self._needs_ir_widget()
+ self.render_widgets()
+
+ def _needs_raman_widget(self):
+ if self._model.needs_raman_tab:
+ self.raman_model = RamanModel()
+ self.raman_widget = RamanWidget(
+ model=self.raman_model,
+ node=self._model.vibro,
+ input_structure=self._model.input_structure,
+ spectrum_type="Raman",
+ )
+ self.children = (*self.children, self.raman_widget)
+
+ def _needs_ir_widget(self):
+ if self._model.needs_ir_tab:
+ self.ir_model = RamanModel()
+ self.ir_widget = RamanWidget(
+ model=self.ir_model,
+ node=self._model.vibro,
+ input_structure=self._model.input_structure,
+ spectrum_type="IR",
+ )
+ self.children = (*self.children, self.ir_widget)
+ else:
+ self.children = (*self.children, ipw.HTML("No IR modes detected."))
+
+ def render_widgets(self):
+ if self._model.needs_raman_tab:
+ self.raman_widget.render()
+ if self._model.needs_ir_tab:
+ self.ir_widget.render()
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
new file mode 100644
index 0000000..c5df792
--- /dev/null
+++ 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]),
+ xaxis2=dict(range=[0, x_max + 0.1]),
+ )
+ self.children = (self.bands_pdos, *self.children)
+ self._model.update_thermo_plot(self.thermal_plot)
diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/ramanmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/ramanmodel.py
new file mode 100644
index 0000000..c1bebfa
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/ramanmodel.py
@@ -0,0 +1,399 @@
+from __future__ import annotations
+from aiidalab_qe.common.mvc import Model
+import traitlets as tl
+from aiida.common.extendeddicts import AttributeDict
+from ase.atoms import Atoms
+from IPython.display import display
+import numpy as np
+from aiida_vibroscopy.utils.broadenings import multilorentz
+import plotly.graph_objects as go
+import base64
+import json
+
+
+class RamanModel(Model):
+ vibro = tl.Instance(AttributeDict, allow_none=True)
+ input_structure = tl.Instance(Atoms, allow_none=True)
+ spectrum_type = tl.Unicode()
+
+ plot_type_options = tl.List(
+ trait=tl.List(tl.Unicode()),
+ default_value=[
+ ("Powder", "powder"),
+ ("Single Crystal", "single_crystal"),
+ ],
+ )
+
+ plot_type = tl.Unicode("powder")
+ temperature = tl.Float(300)
+ frequency_laser = tl.Float(532)
+ pol_incoming = tl.Unicode("0 0 1")
+ pol_outgoing = tl.Unicode("0 0 1")
+ broadening = tl.Float(10.0)
+ separate_polarizations = tl.Bool(False)
+
+ frequencies = []
+ intensities = []
+
+ frequencies_depolarized = []
+ intensities_depolarized = []
+
+ # Active modes
+ active_modes_options = tl.List(
+ trait=tl.Tuple((tl.Unicode(), tl.Int())),
+ )
+ active_mode = tl.Int(0)
+ amplitude = tl.Float(3.0)
+
+ supercell_0 = tl.Int(1)
+ supercell_1 = tl.Int(1)
+ supercell_2 = tl.Int(1)
+
+ def fetch_data(self):
+ """Fetch the Raman data from the VibroWorkChain"""
+ self.raman_data = self.get_vibrational_data(self.vibro)
+ self.raw_frequencies, self.eigenvectors, self.labels = (
+ self.raman_data.run_active_modes(
+ selection_rule=self.spectrum_type.lower(),
+ )
+ )
+ self.rounded_frequencies = [
+ round(frequency, 3) for frequency in self.raw_frequencies
+ ]
+ self.active_modes_options = self._get_active_modes_options()
+
+ def _get_active_modes_options(self):
+ active_modes_options = [
+ (f"{index + 1}: {value}", index)
+ for index, value in enumerate(self.rounded_frequencies)
+ ]
+
+ return active_modes_options
+
+ def update_data(self):
+ """
+ Update the plot data based on the selected spectrum type, plot type, and configuration.
+ """
+ if self.plot_type == "powder":
+ self._update_powder_data()
+ else:
+ self._update_single_crystal_data()
+
+ def _update_powder_data(self):
+ """
+ Update data for the powder plot, handling both Raman and IR spectra.
+ """
+ if self.spectrum_type == "Raman":
+ (
+ polarized_intensities,
+ depolarized_intensities,
+ frequencies,
+ _,
+ ) = self.raman_data.run_powder_raman_intensities(
+ frequencies=self.frequency_laser,
+ temperature=self.temperature,
+ )
+
+ if self.separate_polarizations:
+ self.frequencies, self.intensities = self.generate_plot_data(
+ frequencies,
+ polarized_intensities,
+ self.broadening,
+ )
+ self.frequencies_depolarized, self.intensities_depolarized = (
+ self.generate_plot_data(
+ frequencies,
+ depolarized_intensities,
+ self.broadening,
+ )
+ )
+ else:
+ combined_intensities = polarized_intensities + depolarized_intensities
+ self.frequencies, self.intensities = self.generate_plot_data(
+ frequencies,
+ combined_intensities,
+ self.broadening,
+ )
+ self.frequencies_depolarized, self.intensities_depolarized = [], []
+
+ elif self.spectrum_type == "IR":
+ (
+ intensities,
+ frequencies,
+ _,
+ ) = self.raman_data.run_powder_ir_intensities()
+ self.frequencies, self.intensities = self.generate_plot_data(
+ frequencies,
+ intensities,
+ self.broadening,
+ )
+ self.frequencies_depolarized, self.intensities_depolarized = [], []
+
+ def _update_single_crystal_data(self):
+ """
+ Update data for the single crystal plot, handling both Raman and IR spectra.
+ """
+ dir_incoming, _ = self._check_inputs_correct(self.pol_incoming)
+
+ if self.spectrum_type == "Raman":
+ dir_outgoing, _ = self._check_inputs_correct(self.pol_outgoing)
+ (
+ intensities,
+ frequencies,
+ _,
+ ) = self.raman_data.run_single_crystal_raman_intensities(
+ pol_incoming=dir_incoming,
+ pol_outgoing=dir_outgoing,
+ frequencies=self.frequency_laser,
+ temperature=self.temperature,
+ )
+ elif self.spectrum_type == "IR":
+ (
+ intensities,
+ frequencies,
+ _,
+ ) = self.raman_data.run_single_crystal_ir_intensities(
+ pol_incoming=dir_incoming
+ )
+
+ self.frequencies, self.intensities = self.generate_plot_data(
+ frequencies, intensities
+ )
+ self.frequencies_depolarized, self.intensities_depolarized = [], []
+
+ def update_plot(self, plot):
+ """
+ Update the Raman plot based on the selected plot type and configuration.
+
+ Parameters:
+ plot: The plotly.graph_objs.Figure widget to update.
+ """
+ update_function = (
+ self._update_powder_plot
+ if self.plot_type == "powder"
+ else self._update_single_crystal_plot
+ )
+ update_function(plot)
+
+ def _update_powder_plot(self, plot):
+ """
+ Update the powder Raman plot.
+
+ Parameters:
+ plot: The plotly.graph_objs.Figure widget to update.
+ """
+ if self.separate_polarizations:
+ self._update_polarized_and_depolarized(plot)
+ else:
+ self._clear_depolarized_and_update(plot)
+
+ def _update_polarized_and_depolarized(self, plot):
+ """
+ Update the plot when polarized and depolarized data are separate.
+
+ Parameters:
+ plot: The plotly.graph_objs.Figure widget to update.
+ """
+ if len(plot.data) == 1:
+ self._update_trace(
+ plot.data[0], self.frequencies, self.intensities, "Polarized"
+ )
+ plot.add_trace(
+ go.Scatter(
+ x=self.frequencies_depolarized,
+ y=self.intensities_depolarized,
+ name="Depolarized",
+ )
+ )
+ plot.layout.title.text = f"Powder {self.spectrum_type} Spectrum"
+ elif len(plot.data) == 2:
+ self._update_trace(
+ plot.data[0], self.frequencies, self.intensities, "Polarized"
+ )
+ self._update_trace(
+ plot.data[1],
+ self.frequencies_depolarized,
+ self.intensities_depolarized,
+ "Depolarized",
+ )
+ plot.layout.title.text = f"Powder {self.spectrum_type} Spectrum"
+
+ def _clear_depolarized_and_update(self, plot):
+ """
+ Clear depolarized data and update the plot.
+
+ Parameters:
+ plot: The plotly.graph_objs.Figure widget to update.
+ """
+ if len(plot.data) == 2:
+ self._update_trace(plot.data[0], self.frequencies, self.intensities, "")
+ plot.data[1].x = []
+ plot.data[1].y = []
+ plot.layout.title.text = f"Powder{self.spectrum_type} Spectrum"
+ elif len(plot.data) == 1:
+ self._update_trace(plot.data[0], self.frequencies, self.intensities, "")
+ plot.layout.title.text = f"Powder{self.spectrum_type} Spectrum"
+
+ def _update_single_crystal_plot(self, plot):
+ """
+ Update the single crystal Raman plot.
+
+ Parameters:
+ plot: The plotly.graph_objs.Figure widget to update.
+ """
+ if len(plot.data) == 2:
+ self._update_trace(plot.data[0], self.frequencies, self.intensities, "")
+ plot.data[1].x = []
+ plot.data[1].y = []
+ plot.layout.title.text = f"Single Crystal {self.spectrum_type} Spectrum"
+ elif len(plot.data) == 1:
+ self._update_trace(plot.data[0], self.frequencies, self.intensities, "")
+ plot.layout.title.text = f"Single Crystal {self.spectrum_type} Spectrum"
+
+ def _update_trace(self, trace, x_data, y_data, name):
+ """
+ Helper function to update a single trace in the plot.
+
+ Parameters:
+ trace: The trace to update.
+ x_data: The new x-axis data.
+ y_data: The new y-axis data.
+ name: The name of the trace.
+ """
+ trace.x = x_data
+ trace.y = y_data
+ trace.name = name
+
+ def get_vibrational_data(self, node):
+ """
+ Extract vibrational data from an IRamanWorkChain or HarmonicWorkChain node.
+
+ Parameters:
+ node: The workchain node containing IRaman or Harmonic data.
+
+ Returns:
+ The vibrational accuracy data (vibro) or None if not available.
+ """
+ # Determine the output node
+ output_node = getattr(node, "iraman", None) or getattr(node, "harmonic", None)
+ if not output_node:
+ return None
+
+ # Check for vibrational data and extract accuracy
+ vibrational_data = getattr(output_node, "vibrational_data", None)
+ if not vibrational_data:
+ return None
+
+ # Extract vibrational accuracy (prefer numerical_accuracy_4 if available)
+ vibro = getattr(vibrational_data, "numerical_accuracy_4", None) or getattr(
+ vibrational_data, "numerical_accuracy_2", None
+ )
+
+ return vibro
+
+ def _check_inputs_correct(self, polarization):
+ # Check if the polarization vectors are correct
+ input_text = polarization
+ input_values = input_text.split()
+ dir_values = []
+ if len(input_values) == 3:
+ try:
+ dir_values = [float(i) for i in input_values]
+ return dir_values, True
+ except: # noqa: E722
+ return dir_values, False
+ else:
+ return dir_values, False
+
+ def generate_plot_data(
+ self,
+ frequencies: list[float],
+ intensities: list[float],
+ broadening: float = 10.0,
+ x_range: list[float] | str = "auto",
+ broadening_function=multilorentz,
+ normalize: bool = True,
+ ):
+ frequencies = np.array(frequencies)
+ intensities = np.array(intensities)
+
+ if x_range == "auto":
+ xi = max(0, frequencies.min() - 200)
+ xf = frequencies.max() + 200
+ x_range = np.arange(xi, xf, 1.0)
+
+ y_range = broadening_function(x_range, frequencies, intensities, broadening)
+
+ if normalize:
+ y_range /= y_range.max()
+
+ return x_range, y_range
+
+ def modes_table(self):
+ """Display table with the active modes."""
+ # Create an HTML table with the active modes
+ table_data = [list(x) for x in zip(self.rounded_frequencies, self.labels)]
+ table_html = ""
+ table_html += "Frequencies (cm-1) | Label |
"
+ for row in table_data:
+ table_html += ""
+ for cell in row:
+ table_html += "{} | ".format(cell)
+ table_html += "
"
+ table_html += "
"
+
+ return table_html
+
+ def set_vibrational_mode_animation(self, weas):
+ eigenvector = self.eigenvectors[self.active_mode]
+ phonon_setting = {
+ "eigenvectors": np.array(
+ [[[real_part, 0] for real_part in row] for row in eigenvector]
+ ),
+ "kpoint": [0, 0, 0], # optional
+ "amplitude": self.amplitude,
+ "factor": self.amplitude * 0.6,
+ "nframes": 20,
+ "repeat": [
+ self.supercell_0,
+ self.supercell_1,
+ self.supercell_2,
+ ],
+ "color": "black",
+ "radius": 0.1,
+ }
+ weas.avr.phonon_setting = phonon_setting
+ return weas
+
+ def download_data(self, _=None):
+ filename = "spectra.json"
+ if self.separate_polarizations:
+ my_dict = {
+ "Frequencies cm-1": self.frequencies.tolist(),
+ "Polarized intensities": self.intensities.tolist(),
+ "Depolarized intensities": self.intensities_depolarized.tolist(),
+ }
+ else:
+ my_dict = {
+ "Frequencies cm-1": self.frequencies.tolist(),
+ "Intensities": self.intensities.tolist(),
+ }
+ json_str = json.dumps(my_dict)
+ b64_str = base64.b64encode(json_str.encode()).decode()
+ self._download(payload=b64_str, filename=filename)
+
+ @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/ramanwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/ramanwidget.py
new file mode 100644
index 0000000..503ead8
--- /dev/null
+++ b/src/aiidalab_qe_vibroscopy/app/widgets/ramanwidget.py
@@ -0,0 +1,300 @@
+import ipywidgets as ipw
+from aiidalab_qe_vibroscopy.app.widgets.ramanmodel import RamanModel
+import plotly.graph_objects as go
+from aiidalab_widgets_base.utils import StatusHTML
+from IPython.display import HTML, clear_output, display
+from weas_widget import WeasWidget
+
+
+class RamanWidget(ipw.VBox):
+ """
+ Widget to display Raman properties Tab
+ """
+
+ def __init__(
+ self, model: RamanModel, node: None, input_structure, spectrum_type, **kwargs
+ ):
+ super().__init__(
+ children=[ipw.HTML("Loading Raman data...")],
+ **kwargs,
+ )
+ self._model = model
+ self._model.spectrum_type = spectrum_type
+ self._model.vibro = node
+ self._model.input_structure = input_structure
+ self.rendered = False
+
+ def render(self):
+ if self.rendered:
+ return
+
+ self.guiConfig = {
+ "enabled": True,
+ "components": {
+ "atomsControl": True,
+ "buttons": True,
+ "cameraControls": True,
+ },
+ "buttons": {
+ "fullscreen": True,
+ "download": True,
+ "measurement": True,
+ },
+ }
+
+ self.plot_type = ipw.ToggleButtons(
+ description="Spectrum type:",
+ style={"description_width": "initial"},
+ )
+ ipw.dlink(
+ (self._model, "plot_type_options"),
+ (self.plot_type, "options"),
+ )
+ ipw.link(
+ (self._model, "plot_type"),
+ (self.plot_type, "value"),
+ )
+ self.plot_type.observe(self._on_plot_type_change, names="value")
+ self.temperature = ipw.FloatText(
+ description="Temperature (K):",
+ style={"description_width": "initial"},
+ )
+ ipw.link(
+ (self._model, "temperature"),
+ (self.temperature, "value"),
+ )
+ self.frequency_laser = ipw.FloatText(
+ description="Laser frequency (nm):",
+ style={"description_width": "initial"},
+ )
+ ipw.link(
+ (self._model, "frequency_laser"),
+ (self.frequency_laser, "value"),
+ )
+ self.pol_incoming = ipw.Text(
+ description="Incoming polarization:",
+ style={"description_width": "initial"},
+ layout=ipw.Layout(visibility="hidden"),
+ )
+ ipw.link(
+ (self._model, "pol_incoming"),
+ (self.pol_incoming, "value"),
+ )
+ self.pol_outgoing = ipw.Text(
+ description="Outgoing polarization:",
+ style={"description_width": "initial"},
+ layout=ipw.Layout(visibility="hidden"),
+ )
+ ipw.link(
+ (self._model, "pol_outgoing"),
+ (self.pol_outgoing, "value"),
+ )
+ self.plot_button = ipw.Button(
+ description="Update Plot",
+ icon="pencil",
+ button_style="primary",
+ layout=ipw.Layout(width="auto"),
+ )
+ self.plot_button.on_click(self._on_plot_button_click)
+ self.download_button = ipw.Button(
+ description="Download Data",
+ icon="download",
+ button_style="primary",
+ layout=ipw.Layout(width="auto"),
+ )
+ self.download_button.on_click(self._model.download_data)
+ self._wrong_syntax = StatusHTML(clear_after=8)
+
+ self.broadening = ipw.FloatText(
+ description="Broadening (cm-1):",
+ style={"description_width": "initial"},
+ )
+ ipw.link(
+ (self._model, "broadening"),
+ (self.broadening, "value"),
+ )
+
+ self.separate_polarizations = ipw.Checkbox(
+ description="Separate polarized and depolarized intensities",
+ style={"description_width": "initial"},
+ )
+ ipw.link(
+ (self._model, "separate_polarizations"),
+ (self.separate_polarizations, "value"),
+ )
+ self.spectrum = go.FigureWidget(
+ layout=go.Layout(
+ title=dict(text="Powder Raman spectrum"),
+ barmode="overlay",
+ xaxis=dict(
+ title="Wavenumber (cm-1)",
+ nticks=0,
+ ),
+ yaxis=dict(
+ title="Intensity (arb. units)",
+ ),
+ height=500,
+ width=700,
+ plot_bgcolor="white",
+ )
+ )
+
+ # Active Modes
+ self.modes_table = ipw.Output()
+ self.animation = ipw.Output()
+
+ self.active_modes = ipw.Dropdown(
+ description="Select mode:",
+ style={"description_width": "initial"},
+ )
+ ipw.dlink(
+ (self._model, "active_modes_options"),
+ (self.active_modes, "options"),
+ )
+ self.amplitude = ipw.FloatText(
+ description="Amplitude :",
+ style={"description_width": "initial"},
+ )
+ ipw.link(
+ (self._model, "amplitude"),
+ (self.amplitude, "value"),
+ )
+ self._supercell = [
+ ipw.BoundedIntText(min=1, layout={"width": "40px"}),
+ ipw.BoundedIntText(min=1, layout={"width": "40px"}),
+ ipw.BoundedIntText(min=1, layout={"width": "40px"}),
+ ]
+ for i, widget in enumerate(self._supercell):
+ ipw.link(
+ (self._model, f"supercell_{i}"),
+ (widget, "value"),
+ )
+
+ self.supercell_selector = ipw.HBox(
+ [
+ ipw.HTML(
+ description="Super cell:", style={"description_width": "initial"}
+ )
+ ]
+ + self._supercell
+ )
+ # WeasWidget Setting
+ self.weas = WeasWidget(guiConfig=self.guiConfig)
+ self.weas.from_ase(self._model.input_structure)
+ self.weas.avr.model_style = 1
+ self.weas.avr.color_type = "JMOL"
+
+ widget_list = [
+ self.active_modes,
+ self.amplitude,
+ self._supercell[0],
+ self._supercell[1],
+ self._supercell[2],
+ ]
+ for elem in widget_list:
+ elem.observe(self._select_active_mode, names="value")
+
+ self.children = [
+ ipw.HTML(f"{self._model.spectrum_type} spectroscopy
"),
+ ipw.HTML(
+ """
+ Select the type of Raman spectrum to plot.
+
"""
+ ),
+ self.plot_type,
+ self.temperature,
+ self.frequency_laser,
+ self.broadening,
+ self.separate_polarizations,
+ self.pol_incoming,
+ self.pol_outgoing,
+ ipw.HBox([self.plot_button, self.download_button]),
+ self.spectrum,
+ ipw.HBox(
+ [
+ ipw.VBox(
+ [
+ ipw.HTML(
+ value=f"{self._model.spectrum_type} Active Modes"
+ ),
+ self.modes_table,
+ ]
+ ),
+ ipw.VBox(
+ [
+ self.active_modes,
+ self.amplitude,
+ self.supercell_selector,
+ self.animation,
+ ],
+ ),
+ ]
+ ),
+ ]
+
+ self.rendered = True
+
+ self._initial_view()
+
+ def _initial_view(self):
+ self._model.fetch_data()
+ self._model.update_data()
+ if self._model.spectrum_type == "IR":
+ self.temperature.layout.display = "none"
+ self.frequency_laser.layout.display = "none"
+ self.pol_outgoing.layout.display == "none"
+ self.separate_polarizations.layout.display = "none"
+
+ self.spectrum.add_scatter(
+ x=self._model.frequencies, y=self._model.intensities, name=""
+ )
+ self.spectrum.layout.title.text = f"Powder {self._model.spectrum_type} Spectrum"
+ self.modes_table.layout = {
+ "overflow": "auto",
+ "height": "200px",
+ "width": "150px",
+ }
+ self.weas = self._model.set_vibrational_mode_animation(self.weas)
+ with self.animation:
+ clear_output()
+ display(self.weas)
+
+ with self.modes_table:
+ clear_output()
+ display(HTML(self._model.modes_table()))
+
+ def _on_plot_type_change(self, change):
+ if change["new"] == "single_crystal":
+ self.pol_incoming.layout.visibility = "visible"
+ if self._model.spectrum_type == "Raman":
+ self.pol_outgoing.layout.visibility = "visible"
+ else:
+ self.separate_polarizations.layout.display = "none"
+ self.separate_polarizations.layout.visibility = "hidden"
+ else:
+ self.pol_incoming.layout.visibility = "hidden"
+ self.pol_outgoing.layout.visibility = "hidden"
+ self.separate_polarizations.layout.visibility = "visible"
+
+ def _on_plot_button_click(self, _):
+ _, incoming_syntax_ok = self._model._check_inputs_correct(
+ self.pol_incoming.value
+ )
+ _, outgoing_syntax_ok = self._model._check_inputs_correct(
+ self.pol_outgoing.value
+ )
+ if not (incoming_syntax_ok and outgoing_syntax_ok):
+ self._wrong_syntax.message = """
+
+ ERROR: Invalid syntax for polarization directions.
+
+ """
+ return
+ self._model.update_data()
+ self._model.update_plot(self.spectrum)
+
+ def _select_active_mode(self, _):
+ self.weas = self._model.set_vibrational_mode_animation(self.weas)
+ with self.animation:
+ clear_output()
+ display(self.weas)
diff --git a/src/aiidalab_qe_vibroscopy/app/workchain.py b/src/aiidalab_qe_vibroscopy/app/workchain.py
index 41cd05c..83fcdff 100644
--- a/src/aiidalab_qe_vibroscopy/app/workchain.py
+++ b/src/aiidalab_qe_vibroscopy/app/workchain.py
@@ -44,10 +44,10 @@ def get_builder(codes, structure, parameters):
pw_dielectric_code = codes.get("dielectric")["code"]
phonopy_code = codes.get("phonopy")["code"]
- simulation_mode = parameters["vibronic"].pop("simulation_mode", 1)
+ simulation_mode = parameters["vibronic"].pop("simulation_type", 1)
# Define the supercell matrix
- supercell_matrix = parameters["vibronic"].pop("supercell_selector", None)
+ supercell_matrix = parameters["vibronic"].pop("supercell", None)
# The following include_all is needed to have forces written
overrides = {
diff --git a/src/aiidalab_qe_vibroscopy/utils/dielectric/result.py b/src/aiidalab_qe_vibroscopy/utils/dielectric/result.py
index ecccf70..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
@@ -59,23 +56,18 @@ def get_priority_tensor(filtered_node):
def export_dielectric_data(node):
- if "vibronic" not in node.outputs:
- return None
-
- if not any(
- key in node.outputs.vibronic for key in ["iraman", "dielectric", "harmonic"]
- ):
+ if not any(key in node for key in ["iraman", "dielectric", "harmonic"]):
return None
else:
- if "iraman" in node.outputs.vibronic:
- vibrational_data = node.outputs.vibronic.iraman.vibrational_data
+ if "iraman" in node:
+ vibrational_data = node.iraman.vibrational_data
- elif "harmonic" in node.outputs.vibronic:
- vibrational_data = node.outputs.vibronic.harmonic.vibrational_data
+ elif "harmonic" in node:
+ vibrational_data = node.harmonic.vibrational_data
- elif "dielectric" in node.outputs.vibronic:
- tensor_data = node.outputs.vibronic.dielectric
+ elif "dielectric" in node:
+ tensor_data = node.dielectric
output_data = get_priority_tensor(tensor_data)
dielectric_tensor = output_data.get_array("dielectric").round(
6
@@ -118,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'{cell} | '
- html += "
"
- html += "
"
- 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"])
diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb b/src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb
index 98a0cfc..e66b899 100644
--- a/src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb
+++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb
@@ -8,7 +8,12 @@
"outputs": [
{
"data": {
- "application/javascript": "IPython.OutputArea.prototype._should_scroll = function(lines) {\n return false;\n}\ndocument.title='aiidalab-qe-vibroscopy detached app'\n",
+ "application/javascript": [
+ "IPython.OutputArea.prototype._should_scroll = function(lines) {\n",
+ " return false;\n",
+ "}\n",
+ "document.title='aiidalab-qe-vibroscopy detached app'\n"
+ ],
"text/plain": [
""
]
@@ -30,7 +35,56 @@
"execution_count": 2,
"id": "a8549863",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "a2abe40204ac4a478c99cb6f8ba683b4",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": []
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
Warning:
\n",
+ "
The default profile 'default' was loaded automatically. This behavior will be removed in the v3.0.0. Please load the profile manually before loading modules from aiidalab-widgets-base by adding the following code at the beginning cell of the notebook:
\n",
+ "
\n",
+ "from aiida import load_profile\n",
+ "load_profile();
\n",
+ "
\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "\n",
+ " var style = document.createElement('style');\n",
+ " style.type = 'text/css';\n",
+ " style.innerHTML = ``;\n",
+ " document.head.appendChild(style);\n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"# Fix pybel import path\n",
"try:\n",
@@ -52,10 +106,25 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 3,
"id": "586bd730",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "aec117638bca4fb998fcec2ce07f9d0d",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Output()"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"env = Environment()\n",
"\n",
@@ -66,7 +135,9 @@
" 'Copyright (c) 2024 Miki Bonacci (PSI), miki.bonacci@psi.ch; Version: 0.1.1
'\n",
")\n",
"\n",
- "widget = EuphonicSuperWidget(mode=\"detached\")\n",
+ "from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import EuphonicBaseResultsModel\n",
+ "\n",
+ "widget = EuphonicSuperWidget(mode=\"detached\", model=EuphonicBaseResultsModel())\n",
"\n",
"output = ipw.Output()\n",
"\n",
@@ -75,6 +146,14 @@
"\n",
"display(output)"
]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "22387f54",
+ "metadata": {},
+ "outputs": [],
+ "source": []
}
],
"metadata": {
diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py
index 0d60410..e69de29 100644
--- a/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py
+++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py
@@ -1,334 +0,0 @@
-import pathlib
-import tempfile
-
-
-from IPython.display import display
-
-import ipywidgets as ipw
-
-# from ..euphonic.bands_pdos import *
-from .intensity_maps import (
- generate_force_constant_instance,
- export_euphonic_data, # noqa: F401
-)
-from .euphonic_single_crystal_widgets import SingleCrystalFullWidget
-from .euphonic_powder_widgets import PowderFullWidget
-from .euphonic_q_planes_widgets import QSectionFullWidget
-
-
-###### START for detached app:
-
-# spinner for waiting time (supercell estimations)
-spinner_html = """
-
-
-"""
-
-
-# Upload buttons
-class UploadPhonopyYamlWidget(ipw.FileUpload):
- def __init__(self, **kwargs):
- super().__init__(
- description="upload phonopy YAML file",
- multiple=False,
- layout={"width": "initial"},
- )
-
-
-class UploadForceConstantsHdf5Widget(ipw.FileUpload):
- def __init__(self, **kwargs):
- super().__init__(
- description="upload force constants HDF5 file",
- multiple=False,
- layout={"width": "initial"},
- )
-
-
-class UploadPhonopyWidget(ipw.HBox):
- def __init__(self, **kwargs):
- self.upload_phonopy_yaml = UploadPhonopyYamlWidget(**kwargs)
- self.upload_phonopy_hdf5 = UploadForceConstantsHdf5Widget(**kwargs)
-
- self.reset_uploads = ipw.Button(
- description="Discard uploaded files",
- icon="pencil",
- button_style="warning",
- disabled=False,
- layout=ipw.Layout(width="auto"),
- )
-
- super().__init__(
- children=[
- self.upload_phonopy_yaml,
- self.upload_phonopy_hdf5,
- self.reset_uploads,
- ],
- **kwargs,
- )
-
- def _read_phonopy_files(self, fname, phonopy_yaml_content, fc_hdf5_content=None):
- suffix = "".join(pathlib.Path(fname).suffixes)
-
- with tempfile.NamedTemporaryFile(suffix=suffix) as temp_yaml:
- temp_yaml.write(phonopy_yaml_content)
- temp_yaml.flush()
-
- if fc_hdf5_content:
- with tempfile.NamedTemporaryFile(suffix=".hdf5") as temp_file:
- temp_file.write(fc_hdf5_content)
- temp_file.flush()
- temp_hdf5_name = temp_file.name
-
- try:
- fc = generate_force_constant_instance(
- path=pathlib.Path(fname),
- summary_name=temp_yaml.name,
- fc_name=temp_hdf5_name,
- )
- except ValueError:
- return None
-
- return fc
- else:
- temp_hdf5_name = None
-
- try:
- fc = generate_force_constant_instance(
- path=pathlib.Path(fname),
- summary_name=temp_yaml.name,
- # fc_name=temp_hdf5_name,
- )
- except ValueError:
- return None
-
- return fc
-
-
-#### END for detached app
-
-
-##### START OVERALL WIDGET TO DISPLAY EVERYTHING:
-
-
-class EuphonicSuperWidget(ipw.VBox):
- """
- Widget that will include everything,
- from the upload widget to the tabs with single crystal and powder predictions.
- In between, we trigger the initialization of plots via a button.
- """
-
- def __init__(self, mode="aiidalab-qe app plugin", fc=None, q_path=None):
- """
- Initialize the Euphonic utility class.
- Parameters:
- -----------
- mode : str, optional
- The mode of operation, default is "aiidalab-qe app plugin".
- fc : optional
- Force constants, default is None.
- q_path : optional
- Q-path for phonon calculations, default is None. If Low-D system, this can be provided.
- It is the same path obtained for the PhonopyCalculation of the phonopy_bands.
- Attributes:
- -----------
- mode : str
- The mode of operation.
- upload_widget : UploadPhonopyWidget
- Widget for uploading phonopy files.
- fc_hdf5_content : None
- Content of the force constants HDF5 file.
- tab_widget : ipw.Tab
- Tab widget for different views.
- plot_button : ipw.Button
- Button to initialize INS data.
- fc : optional
- Force constants if provided.
- """
-
- self.mode = mode
-
- self.upload_widget = UploadPhonopyWidget()
- self.upload_widget.reset_uploads.on_click(self._on_reset_uploads_button_clicked)
- self.fc_hdf5_content = None
-
- self.tab_widget = ipw.Tab()
- self.tab_widget.layout.display = "none"
- self.tab_widget.set_title(0, "Single crystal")
- self.tab_widget.set_title(1, "Powder sample")
- self.tab_widget.set_title(2, "Q-plane view")
- self.tab_widget.children = ()
-
- if fc:
- self.fc = fc
-
- self.q_path = q_path
-
- self.plot_button = ipw.Button(
- description="Initialise INS data",
- icon="pencil",
- button_style="primary",
- disabled=True,
- layout=ipw.Layout(width="auto"),
- )
- self.plot_button.on_click(self._on_first_plot_button_clicked)
-
- self.loading_widget = ipw.HTML(
- value=spinner_html,
- )
- self.loading_widget.layout.display = "none"
-
- if self.mode == "aiidalab-qe app plugin":
- self.upload_widget.layout.display = "none"
- self.plot_button.disabled = False
- else:
- self.upload_widget.children[0].observe(self._on_upload_yaml, "_counter")
- self.upload_widget.children[1].observe(self._on_upload_hdf5, "_counter")
-
- super().__init__(
- children=[
- self.upload_widget,
- self.plot_button,
- self.loading_widget,
- self.tab_widget,
- ],
- )
-
- def _on_reset_uploads_button_clicked(self, change):
- self.upload_widget.upload_phonopy_yaml.value.clear()
- self.upload_widget.upload_phonopy_yaml._counter = 0
- self.upload_widget.upload_phonopy_hdf5.value.clear()
- self.upload_widget.upload_phonopy_hdf5._counter = 0
-
- self.plot_button.layout.display = "block"
- self.plot_button.disabled = True
-
- self.tab_widget.children = ()
-
- self.tab_widget.layout.display = "none"
-
- def _on_upload_yaml(self, change):
- if change["new"] != change["old"]:
- for fname in self.upload_widget.children[
- 0
- ].value.keys(): # always one key because I allow only one file at the time.
- self.fname = fname
- self.phonopy_yaml_content = self.upload_widget.children[0].value[fname][
- "content"
- ]
-
- if self.plot_button.disabled:
- self.plot_button.disabled = False
-
- def _on_upload_hdf5(self, change):
- if change["new"] != change["old"]:
- for fname in self.upload_widget.children[1].value.keys():
- self.fc_hdf5_content = self.upload_widget.children[1].value[fname][
- "content"
- ]
-
- def _generate_force_constants(
- self,
- ):
- if self.mode == "aiidalab-qe app plugin":
- return self.fc
-
- else:
- fc = self.upload_widget._read_phonopy_files(
- fname=self.fname,
- phonopy_yaml_content=self.phonopy_yaml_content,
- fc_hdf5_content=self.fc_hdf5_content,
- )
-
- return fc
-
- def _on_first_plot_button_clicked(self, change=None):
- # It creates the widgets
- self.plot_button.layout.display = "none"
-
- self.loading_widget.layout.display = "block"
-
- self.fc = self._generate_force_constants()
-
- # I first initialise this widget, to then have the 0K ref for the other two.
- singlecrystalwidget = SingleCrystalFullWidget(self.fc, self.q_path)
-
- self.tab_widget.children = (
- singlecrystalwidget,
- PowderFullWidget(
- self.fc, intensity_ref_0K=singlecrystalwidget.intensity_ref_0K
- ),
- QSectionFullWidget(
- self.fc, intensity_ref_0K=singlecrystalwidget.intensity_ref_0K
- ),
- )
-
- self.loading_widget.layout.display = "none"
-
- self.tab_widget.layout.display = "block"
-
-
-class DownloadYamlHdf5Widget(ipw.HBox):
- def __init__(self, phonopy_node, **kwargs):
- self.download_button = ipw.Button(
- description="Download phonopy data",
- icon="pencil",
- button_style="primary",
- disabled=False,
- layout=ipw.Layout(width="auto"),
- )
- self.download_button.on_click(self.download_data)
- self.node = phonopy_node
-
- super().__init__(
- children=[
- self.download_button,
- ],
- )
-
- def download_data(self, _=None):
- """
- Download both the phonopy.yaml and fc.hdf5 files.
- """
- phonopy_yaml, fc_hdf5 = generate_force_constant_instance(
- self.node, mode="download"
- )
- self._download(payload=phonopy_yaml, filename="phonopy" + ".yaml")
- self._download(payload=fc_hdf5, filename="fc" + ".hdf5")
-
- @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/utils/euphonic/euphonic_base_widgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py
similarity index 50%
rename from src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_base_widgets.py
rename to src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py
index 1958068..b0daf63 100644
--- a/src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_base_widgets.py
+++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py
@@ -1,10 +1,9 @@
-from IPython.display import display
-
import numpy as np
import ipywidgets as ipw
+import plotly.graph_objects as go
# from ..euphonic.bands_pdos import *
-from .intensity_maps import * # noqa: F403
+from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import * # noqa: F403
# sys and os used to prevent euphonic to print in the stdout.
@@ -43,38 +42,27 @@ class StructureFactorBasePlotWidget(ipw.VBox):
"""
THz_to_meV = 4.13566553853599 # conversion factor.
+ THz_to_cm1 = 33.3564095198155 # conversion factor.
- def __init__(self, final_xspectra, **kwargs):
- """
+ def __init__(self, model):
+ super().__init__()
+ self._model = model
+ self.rendered = False
- Args:
- final_xspectra (_type_):
+ def render(self):
+ """Render the widget.
+ This is the generic render method which can be overwritten by the subwidgets.
+ However, it is important to call this method at the start of the subwidgets.render() in order to have the go.FigureWidget.
"""
- self.message_fig = ipw.HTML("")
- self.message_fig.layout.display = "none"
-
- if self.fig.layout.images:
- for image in self.fig.layout.images:
- image["scl"] = 2 # Set the scale for each image
-
- self.fig["layout"]["xaxis"].update(
- range=[min(final_xspectra), max(final_xspectra)]
- )
-
- self.fig["layout"]["yaxis"].update(title="meV")
+ if self.rendered:
+ return
- self.fig.update_layout(
- height=500,
- width=700,
- margin=dict(l=15, r=15, t=15, b=15),
- )
- # Update x-axis and y-axis to enable autoscaling
- self.fig.update_xaxes(autorange=True)
- self.fig.update_yaxes(autorange=True)
+ if not hasattr(self._model, "fc"):
+ self._model.fetch_data()
+ self._model._update_spectra()
- # Update the layout to enable autoscaling
- self.fig.update_layout(autosize=True)
+ self.fig = go.FigureWidget()
self.slider_intensity = ipw.FloatRangeSlider(
value=[1, 100], # Default selected interval
@@ -108,52 +96,30 @@ def __init__(self, final_xspectra, **kwargs):
),
)
self.E_units_button.observe(self._update_energy_units, "value")
+ # MAYBE WE LINK ALSO THIS TO THE MODEL? so we can download the data with the preferred units.
# Create and show figure
- super().__init__(
- children=[
- self.message_fig,
- self.fig,
- ipw.HBox([ipw.HTML("Intensity window (%):"), self.slider_intensity]),
- self.specification_intensity,
- self.E_units_button,
- ],
- layout=ipw.Layout(
- width="100%",
- ),
- )
+ self.children = [
+ self.fig,
+ ipw.HBox([ipw.HTML("Intensity window (%):"), self.slider_intensity]),
+ self.specification_intensity,
+ self.E_units_button,
+ ]
- def _update_spectra(
- self,
- final_zspectra,
- ):
- # this will be called in the _update_spectra method of SingleCrystalPlotWidget and PowderPlotWidget
+ def _update_plot(self):
+ """This is the generic update_plot method which can be overwritten by the subwidgets.
+ However, it is important to call this method at the end of the subwidgets._update_plot() in order to update the intensity window.
+ """
- # Update the layout to enable autoscaling
self.fig.update_layout(autosize=True)
- # We should do a check, if we have few points (<200?) provide like a warning..
- # Also decise less than what, 30%, 50%...?
-
- """
- visible_points = len(
- np.where(self.fig.data[0].z > 0.5)[0]
- )
- if visible_points < 1000:
- message = f"Only {visible_points}/{len(final_zspectra.T)} points have intensity higher than 50%"
- self.message_fig.value = message
- self.message_fig.layout.display = "block"
- else:
- self.message_fig.layout.display = "none"
- """
-
# I have also to update the energy window. or better, to set the intensity to respect the current intensity window selected:
self.fig.data[0].zmax = (
self.slider_intensity.value[1] * np.max(self.fig.data[0].z) / 100
- ) # above this, it is all yellow, i.e. max intensity.
+ ) # above this, it is all yellow, i.e. this is the max detachable intensity.
self.fig.data[0].zmin = (
self.slider_intensity.value[0] * np.max(self.fig.data[0].z) / 100
- ) # above this, it is all yellow, i.e. max intensity.
+ ) # below this, it is all dark blue, i.e. this is the min detachable intensity.
def _update_intensity_filter(self, change):
# the value of the intensity slider is in fractions of the max.
@@ -186,50 +152,78 @@ class StructureFactorSettingsBaseWidget(ipw.VBox):
both single crystal or powder.
"""
- def __init__(self, **kwargs):
+ def __init__(self, model, **kwargs):
super().__init__()
+ self._model = model
+ self.rendered = False
- self.float_q_spacing = ipw.FloatText(
- value=0.01,
+ def render(self):
+ """Render the widget."""
+
+ if self.rendered:
+ return
+
+ self.q_spacing = ipw.FloatText(
+ value=self._model.q_spacing,
step=0.001,
description="q step (1/A)",
tooltip="q spacing in 1/A",
)
- self.float_q_spacing.observe(self._on_setting_changed, names="value")
+ ipw.link(
+ (self._model, "q_spacing"),
+ (self.q_spacing, "value"),
+ )
+ self.q_spacing.observe(self._on_setting_changed, names="value")
- self.float_energy_broadening = ipw.FloatText(
- value=0.5,
+ self.energy_broadening = ipw.FloatText(
+ value=self._model.energy_broadening,
step=0.01,
description="ΔE (meV)",
tooltip="Energy broadening in meV",
)
- self.float_energy_broadening.observe(self._on_setting_changed, names="value")
+ ipw.link(
+ (self._model, "energy_broadening"),
+ (self.energy_broadening, "value"),
+ )
+ self.energy_broadening.observe(self._on_setting_changed, names="value")
- self.int_energy_bins = ipw.IntText(
- value=200,
+ self.energy_bins = ipw.IntText(
+ value=self._model.energy_bins,
description="#E bins",
tooltip="Number of energy bins",
)
- self.int_energy_bins.observe(self._on_setting_changed, names="value")
+ ipw.link(
+ (self._model, "energy_bins"),
+ (self.energy_bins, "value"),
+ )
+ self.energy_bins.observe(self._on_setting_changed, names="value")
- self.float_T = ipw.FloatText(
- value=0,
+ self.temperature = ipw.FloatText(
+ value=self._model.temperature,
step=0.01,
description="T (K)",
disabled=False,
)
- self.float_T.observe(self._on_setting_changed, names="value")
+ ipw.link(
+ (self._model, "temperature"),
+ (self.temperature, "value"),
+ )
+ self.temperature.observe(self._on_setting_changed, names="value")
self.weight_button = ipw.ToggleButtons(
options=[
("Coherent", "coherent"),
("DOS", "dos"),
],
- value="coherent",
+ value=self._model.weighting,
description="weight:",
disabled=False,
style={"description_width": "initial"},
)
+ ipw.link(
+ (self._model, "weighting"),
+ (self.weight_button, "value"),
+ )
self.weight_button.observe(self._on_weight_button_change, names="value")
self.plot_button = ipw.Button(
@@ -248,6 +242,7 @@ def __init__(self, **kwargs):
disabled=False,
layout=ipw.Layout(width="auto"),
)
+ self.reset_button.on_click(self._reset_settings)
self.download_button = ipw.Button(
description="Download Data and Plot",
@@ -257,97 +252,20 @@ def __init__(self, **kwargs):
layout=ipw.Layout(width="auto"),
)
- self.reset_button.on_click(self._reset_settings)
-
- def _reset_settings(self, _):
- self.float_q_spacing.value = 0.01
- self.float_energy_broadening.value = 0.5
- self.int_energy_bins.value = 200
- self.float_T.value = 0
- self.weight_button.value = "coherent"
-
def _on_plot_button_changed(self, change):
if change["new"] != change["old"]:
self.download_button.disabled = not change["new"]
def _on_weight_button_change(self, change):
if change["new"] != change["old"]:
- self.float_T.value = 0
- self.float_T.disabled = True if change["new"] == "dos" else False
+ self._model.temperature = 0
+ self.temperature.disabled = True if change["new"] == "dos" else False
self.plot_button.disabled = False
- def _on_setting_changed(self, change):
+ def _on_setting_changed(
+ self, change
+ ): # think if we want to do something more evident...
self.plot_button.disabled = False
-
-class SingleCrystalSettingsWidget(StructureFactorSettingsBaseWidget):
- def __init__(self, **kwargs):
- self.custom_kpath_description = ipw.HTML(
- """
-
- Custom q-points path for the structure factor:
- we can provide it via a specific format:
- (1) each linear path should be divided by '|';
- (2) each path is composed of 'qxi qyi qzi - qxf qyf qzf' where qxi and qxf are, respectively,
- the start and end x-coordinate of the q direction, in reciprocal lattice units (rlu).
- An example path is: '0 0 0 - 1 1 1 | 1 1 1 - 0.5 0.5 0.5'.
- For now, we do not support fractions (i.e. we accept 0.5 but not 1/2).
-
- """
- )
-
- self.custom_kpath_text = ipw.Text(
- value="",
- description="Custom path (rlu):",
- style={"description_width": "initial"},
- )
- custom_style = ''
- display(ipw.HTML(custom_style))
- self.custom_kpath_text.add_class("custom-font")
-
- self.custom_kpath_text.observe(self._on_setting_changed, names="value")
-
- # Please note: if you change the order of the widgets below, it will
- # affect the usage of the children[0] below in the full widget.
-
- super().__init__()
-
- self.children = [
- ipw.HBox(
- [
- ipw.VBox(
- [
- ipw.HBox(
- [
- self.reset_button,
- self.plot_button,
- self.download_button,
- ]
- ),
- self.specification_intensity,
- self.float_q_spacing,
- self.float_energy_broadening,
- self.int_energy_bins,
- self.float_T,
- self.weight_button,
- ],
- layout=ipw.Layout(
- width="50%",
- ),
- ),
- ipw.VBox(
- [
- self.custom_kpath_description,
- self.custom_kpath_text,
- ],
- layout=ipw.Layout(
- width="80%",
- ),
- ),
- ], # end of HBox children
- ),
- ]
-
def _reset_settings(self, _):
- self.custom_kpath_text.value = ""
- super()._reset_settings(_)
+ self._model.reset()
diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/bands_pdos.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/bands_pdos.py
similarity index 100%
rename from src/aiidalab_qe_vibroscopy/utils/euphonic/bands_pdos.py
rename to src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/bands_pdos.py
diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/intensity_maps.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py
similarity index 98%
rename from src/aiidalab_qe_vibroscopy/utils/euphonic/intensity_maps.py
rename to src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py
index 6a7490b..66095ca 100644
--- a/src/aiidalab_qe_vibroscopy/utils/euphonic/intensity_maps.py
+++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py
@@ -231,7 +231,9 @@ def produce_bands_weigthed_data(
if not params:
args = AttrDict(copy.deepcopy(parameters))
else:
- args = AttrDict(copy.deepcopy(params))
+ args = copy.deepcopy(parameters)
+ args.update(params)
+ args = AttrDict(args)
# redundancy with args...
calc_modes_kwargs = _calc_modes_kwargs(args)
@@ -431,6 +433,7 @@ def produce_powder_data(
params: Optional[List[str]] = parameters_powder,
fc: ForceConstants = None,
plot=False,
+ linear_path=None,
) -> None:
blockPrint()
@@ -758,18 +761,14 @@ def generate_force_constant_instance(
return fc
-def export_euphonic_data(node, fermi_energy=None):
- if "vibronic" not in node.outputs:
- # Not a phonon calculation
+def export_euphonic_data(output_vibronic, fermi_energy=None):
+ if "phonon_bands" not in output_vibronic:
return None
- else:
- if "phonon_bands" not in node.outputs.vibronic:
- return None
- output_set = node.outputs.vibronic.phonon_bands
+ output_set = output_vibronic.phonon_bands
- if any(not element for element in node.inputs.structure.pbc):
- vibro_bands = node.inputs.vibronic.phonopy_bands_dict.get_dict()
+ if any(not element for element in output_set.creator.caller.inputs.structure.pbc):
+ vibro_bands = output_set.creator.caller.inputs.phonopy_bands_dict.get_dict()
# Group the band and band_labels
band = vibro_bands["band"]
band_labels = vibro_bands["band_labels"]
diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_powder_widgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_powder_widgets.py
similarity index 51%
rename from src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_powder_widgets.py
rename to src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_powder_widgets.py
index 8543482..cf218f1 100644
--- a/src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_powder_widgets.py
+++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_powder_widgets.py
@@ -5,17 +5,15 @@
import ipywidgets as ipw
import plotly.graph_objects as go
import plotly.io as pio
-import numpy as np
# from ..euphonic.bands_pdos import *
-from ..euphonic.intensity_maps import produce_powder_data, parameters_powder, AttrDict
import json
from monty.json import jsanitize
# sys and os used to prevent euphonic to print in the stdout.
-from aiidalab_qe_vibroscopy.utils.euphonic.euphonic_base_widgets import (
+from aiidalab_qe_vibroscopy.utils.euphonic.base_widgets.euphonic_base_widgets import (
StructureFactorBasePlotWidget,
StructureFactorSettingsBaseWidget,
COLORBAR_DICT,
@@ -24,98 +22,119 @@
class PowderPlotWidget(StructureFactorBasePlotWidget):
- def __init__(self, spectra, intensity_ref_0K=1, **kwargs):
- final_zspectra = spectra.z_data.magnitude
- final_xspectra = spectra.x_data.magnitude
- # Data to contour is the sum of two Gaussian functions.
- x, y = np.meshgrid(spectra.x_data.magnitude, spectra.y_data.magnitude)
+ def render(self):
+ if self.rendered:
+ return
+
+ super().render()
+
+ heatmap_trace = go.Heatmap(
+ z=self._model.final_zspectra.T,
+ y=self._model.y[:, 0] * self.THz_to_meV,
+ x=self._model.x[0],
+ colorbar=COLORBAR_DICT,
+ colorscale=COLORSCALE, # imported from euphonic_base_widgets
+ )
- self.intensity_ref_0K = intensity_ref_0K
+ # Add colorbar
+ colorbar = heatmap_trace.colorbar
+ colorbar.x = 1.05 # Move colorbar to the right
+ colorbar.y = 0.5 # Center colorbar vertically
- self.fig = go.FigureWidget()
+ # Add heatmap trace to figure
+ self.fig.add_trace(heatmap_trace)
- self.fig.add_trace(
- go.Heatmap(
- z=final_zspectra.T,
- y=y[:, 0] * self.THz_to_meV,
- x=x[0],
- colorbar=COLORBAR_DICT,
- colorscale=COLORSCALE, # imported from euphonic_base_widgets
- )
+ # Layout settings
+ self.fig["layout"]["xaxis"].update(
+ title="|q| (1/A)",
+ range=[min(self._model.final_xspectra), max(self._model.final_xspectra)],
+ )
+ self.fig["layout"]["yaxis"].update(
+ title="meV",
+ range=[min(self._model.y[:, 0]), max(self._model.y[:, 0])],
)
- self.fig["layout"]["xaxis"].update(title="|q| (1/A)")
- self.fig["layout"]["yaxis"].update(range=[min(y[:, 0]), max(y[:, 0])])
+ if self.fig.layout.images:
+ for image in self.fig.layout.images:
+ image["scl"] = 2 # Set the scale for each image
- # Create and show figure
- super().__init__(
- final_xspectra,
- **kwargs,
+ self.fig.update_layout(
+ height=500,
+ width=700,
+ margin=dict(l=15, r=15, t=15, b=15),
)
+ # Update x-axis and y-axis to enable autoscaling
+ self.fig.update_xaxes(autorange=True)
+ self.fig.update_yaxes(autorange=True)
- def _update_spectra(self, spectra):
- final_zspectra = spectra.z_data.magnitude
- final_xspectra = spectra.x_data.magnitude # noqa: F841
- # Data to contour is the sum of two Gaussian functions.
- x, y = np.meshgrid(spectra.x_data.magnitude, spectra.y_data.magnitude)
+ # Update the layout to enable autoscaling
+ self.fig.update_layout(autosize=True)
- # If I do this
- # self.data = ()
- # I have a delay in the plotting, I have blank plot while it
- # is adding the new trace (see below); So, I will instead do the
- # re-assignement of the self.data = [self.data[1]] afterwards.
+ def _update_plot(self):
+ # update the spectra, i.e. the data contained in the _model.
+ self._model._update_spectra()
- x = x[0]
self.fig.add_trace(
go.Heatmap(
- z=final_zspectra.T,
+ z=self._model.final_zspectra.T,
y=(
- y[:, 0] * self.THz_to_meV
+ self._model.y[:, 0] * self.THz_to_meV
if self.E_units_button.value == "meV"
- else y[:, 0]
+ else self._model.y[:, 0]
),
- x=x,
+ x=self._model.x[0],
colorbar=COLORBAR_DICT,
colorscale=COLORSCALE, # imported from euphonic_base_widgets
)
)
- # change the path wants also a change in the labels.
- # this is delays things
- self.fig["layout"]["xaxis"].update(title="|q| (1/A)")
-
self.fig.data = [self.fig.data[1]]
- super()._update_spectra(final_zspectra)
+ super()._update_plot()
class PowderSettingsWidget(StructureFactorSettingsBaseWidget):
- def __init__(self, **kwargs):
- self.float_qmin = ipw.FloatText(
+ def render(self):
+ if self.rendered:
+ return
+
+ super().render()
+
+ self.qmin = ipw.FloatText(
value=0,
description="|q|min (1/A)",
)
- self.float_qmin.observe(self._on_setting_changed, names="value")
+ ipw.link(
+ (self._model, "q_min"),
+ (self.qmin, "value"),
+ )
+ self.qmin.observe(self._on_setting_changed, names="value")
- self.float_qmax = ipw.FloatText(
+ self.qmax = ipw.FloatText(
step=0.01,
value=1,
description="|q|max (1/A)",
)
- self.float_qmax.observe(self._on_setting_changed, names="value")
+ ipw.link(
+ (self._model, "q_max"),
+ (self.qmax, "value"),
+ )
+ self.qmax.observe(self._on_setting_changed, names="value")
self.int_npts = ipw.IntText(
value=100,
description="npts",
tooltip="Number of points to be used in the average sphere.",
)
+ ipw.link(
+ (self._model, "npts"),
+ (self.int_npts, "value"),
+ )
self.int_npts.observe(self._on_setting_changed, names="value")
# Please note: if you change the order of the widgets below, it will
# affect the usage of the children[0] below in the full widget.
- super().__init__()
-
self.children = [
ipw.HBox(
[
@@ -128,9 +147,9 @@ def __init__(self, **kwargs):
self.download_button,
]
),
- self.float_q_spacing,
- self.float_qmin,
- self.float_qmax,
+ self.q_spacing,
+ self.qmin,
+ self.qmax,
# self.int_npts,
],
layout=ipw.Layout(
@@ -139,9 +158,9 @@ def __init__(self, **kwargs):
),
ipw.VBox(
[
- self.float_energy_broadening,
- self.int_energy_bins,
- self.float_T,
+ self.energy_broadening,
+ self.energy_bins,
+ self.temperature,
self.weight_button,
],
layout=ipw.Layout(
@@ -152,12 +171,6 @@ def __init__(self, **kwargs):
),
]
- def _reset_settings(self, _):
- self.float_qmin.value = 0
- self.float_qmax.value = 1
- self.int_npts.value = 100
- super()._reset_settings(_)
-
class PowderFullWidget(ipw.VBox):
"""
@@ -169,27 +182,30 @@ class PowderFullWidget(ipw.VBox):
and are from Sears (1992) Neutron News 3(3) pp26--37.
"""
- def __init__(self, fc, intensity_ref_0K=1, **kwargs):
- self.fc = fc
+ def __init__(self, model):
+ self._model = model
+ self.rendered = False
+ super().__init__()
- self.spectra, self.parameters = produce_powder_data(
- params=parameters_powder, fc=self.fc
- )
+ def render(self):
+ if self.rendered:
+ return
self.title_intensity = ipw.HTML(
"Neutron dynamic structure factor - Powder sample
"
)
- self.settings_intensity = PowderSettingsWidget(mode="powder")
- self.settings_intensity.plot_button.on_click(self._on_plot_button_clicked)
- self.settings_intensity.download_button.on_click(self.download_data)
+ # we initialize and inject the model here.
+ self.settings_intensity = PowderSettingsWidget(model=self._model)
+ self.map_widget = PowderPlotWidget(model=self._model)
- # This is used in order to have an overall intensity scale. Inherithed from the SingleCrystal
- self.intensity_ref_0K = intensity_ref_0K # CHANGED
+ # rendering the widgets
+ self.settings_intensity.render()
+ self.map_widget.render()
- self.map_widget = PowderPlotWidget(
- self.spectra, mode="powder", intensity_ref_0K=self.intensity_ref_0K
- ) # CHANGED
+ # specific for the powder
+ self.settings_intensity.plot_button.on_click(self._on_plot_button_clicked)
+ self.settings_intensity.download_button.on_click(self.download_data)
super().__init__(
children=[
@@ -199,27 +215,11 @@ def __init__(self, fc, intensity_ref_0K=1, **kwargs):
],
)
- def _on_plot_button_clicked(self, change=None):
- self.parameters.update(
- {
- "weighting": self.settings_intensity.weight_button.value,
- "q_spacing": self.settings_intensity.float_q_spacing.value,
- "energy_broadening": self.settings_intensity.float_energy_broadening.value,
- "ebins": self.settings_intensity.int_energy_bins.value,
- "temperature": self.settings_intensity.float_T.value,
- "q_min": self.settings_intensity.float_qmin.value,
- "q_max": self.settings_intensity.float_qmax.value,
- "npts": self.settings_intensity.int_npts.value,
- }
- )
- parameters_powder = AttrDict(self.parameters)
-
- self.spectra, self.parameters = produce_powder_data(
- params=parameters_powder, fc=self.fc, plot=False
- )
+ self.rendered = True
+ def _on_plot_button_clicked(self, change=None):
self.settings_intensity.plot_button.disabled = True
- self.map_widget._update_spectra(self.spectra) # CHANGED
+ self.map_widget._update_plot()
def download_data(self, _=None):
"""
@@ -229,18 +229,7 @@ def download_data(self, _=None):
filename = "powder"
my_dict = self.spectra.to_dict()
- my_dict.update(
- {
- "weighting": self.settings_intensity.weight_button.value,
- "q_spacing": self.settings_intensity.float_q_spacing.value,
- "energy_broadening": self.settings_intensity.float_energy_broadening.value,
- "ebins": self.settings_intensity.int_energy_bins.value,
- "temperature": self.settings_intensity.float_T.value,
- "q_min": self.settings_intensity.float_qmin.value,
- "q_max": self.settings_intensity.float_qmax.value,
- # "npts": self.settings_intensity.int_npts.value,
- }
- )
+ my_dict.update(self._model.get_model_state())
for k in ["weighting", "q_spacing", "temperature"]:
filename += "_" + k + "_" + str(my_dict[k])
diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_q_planes_widgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_q_planes_widgets.py
similarity index 70%
rename from src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_q_planes_widgets.py
rename to src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_q_planes_widgets.py
index c54199c..6fa8a5f 100644
--- a/src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_q_planes_widgets.py
+++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_q_planes_widgets.py
@@ -11,7 +11,7 @@
_get_energy_bins,
)
-from aiidalab_qe_vibroscopy.utils.euphonic.euphonic_base_widgets import (
+from aiidalab_qe_vibroscopy.utils.euphonic.base_widgets.euphonic_base_widgets import (
StructureFactorSettingsBaseWidget,
COLORSCALE,
COLORBAR_DICT,
@@ -21,10 +21,9 @@
from monty.json import jsanitize
import plotly.graph_objects as go
-from aiidalab_qe_vibroscopy.utils.euphonic.intensity_maps import (
+from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import (
blockPrint,
enablePrint,
- AttrDict,
)
@@ -102,7 +101,7 @@ def produce_Q_section_spectrum(
dw=None,
labels=None,
):
- from aiidalab_qe_vibroscopy.utils.euphonic.intensity_maps import (
+ from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import (
blockPrint,
enablePrint,
)
@@ -134,27 +133,24 @@ def produce_Q_section_spectrum(
class QSectionPlotWidget(StructureFactorBasePlotWidget):
- def __init__(self, h_array, k_array, av_spec, labels, intensity_ref_0K=1, **kwargs):
- self.intensity_ref_0K = intensity_ref_0K
+ def render(
+ self,
+ ):
+ if self.rendered:
+ return
- self.fig = go.FigureWidget()
+ super().render()
self.fig.add_trace(
go.Heatmap(
- z=av_spec,
- x=h_array,
- y=k_array,
+ z=self._model.av_spec,
+ x=self._model.h_array,
+ y=self._model.k_array,
colorbar=COLORBAR_DICT,
colorscale=COLORSCALE, # imported from euphonic_base_widgets
)
)
- # Create and show figure
- super().__init__(
- h_array,
- **kwargs,
- )
-
self.fig.update_layout(
height=625,
width=650,
@@ -165,32 +161,20 @@ def __init__(self, h_array, k_array, av_spec, labels, intensity_ref_0K=1, **kwar
self.E_units_button.layout.display = "none"
# self.fig.update_layout(title=labels["q"])
self.fig["layout"]["xaxis"].update(
- range=[np.min(h_array), np.max(h_array)],
+ range=[np.min(self._model.h_array), np.max(self._model.h_array)],
title=r"$\alpha \text{ in } \vec{Q_0} + \alpha \vec{h}$",
)
self.fig["layout"]["yaxis"].update(
- range=[np.min(k_array), np.max(k_array)],
+ range=[np.min(self._model.k_array), np.max(self._model.k_array)],
title=r"$\beta \text{ in } \vec{Q_0} + \beta \vec{k}$",
)
- def _update_spectra(
- self,
- h_array,
- k_array,
- av_spec,
- labels,
- ):
- # If I do this
- # self.data = ()
- # I have a delay in the plotting, I have blank plot while it
- # is adding the new trace (see below); So, I will instead do the
- # re-assignement of the self.data = [self.data[1]] afterwards.
-
+ def _update_plot(self):
self.fig.add_trace(
go.Heatmap(
- z=av_spec,
- x=h_array,
- y=k_array,
+ z=self._model.av_spec,
+ x=self._model.h_array,
+ y=self._model.k_array,
colorbar=COLORBAR_DICT,
colorscale=COLORSCALE, # imported from euphonic_base_widgets
)
@@ -203,28 +187,33 @@ def _update_spectra(
)
# self.fig.update_layout(title=labels["q"])
self.fig["layout"]["xaxis"].update(
- range=[np.min(h_array), np.max(h_array)],
+ range=[np.min(self._model.h_array), np.max(self._model.h_array)],
title=r"$\alpha \text{ in } \vec{Q_0} + \alpha \vec{h}$",
)
self.fig["layout"]["yaxis"].update(
- range=[np.min(k_array), np.max(k_array)],
+ range=[np.min(self._model.k_array), np.max(self._model.k_array)],
title=r"$\beta \text{ in } \vec{Q_0} + \beta \vec{k}$",
)
self.fig.data = [self.fig.data[1]]
- # super()._update_spectra(av_spec, intensity_ref_0K=intensity_ref_0K)
-
class QSectionSettingsWidget(StructureFactorSettingsBaseWidget):
- def __init__(self, **kwargs):
- super().__init__()
+ def render(self):
+ if self.rendered:
+ return
+
+ super().render()
- self.float_ecenter = ipw.FloatText(
+ self.ecenter = ipw.FloatText(
value=0,
description="E (meV)",
)
- self.float_ecenter.observe(self._on_setting_changed, names="value")
+ ipw.link(
+ (self._model, "center_e"),
+ (self.ecenter, "value"),
+ )
+ self.ecenter.observe(self._on_setting_changed, names="value")
self.plane_description_widget = ipw.HTML(
"""
@@ -271,6 +260,7 @@ def __init__(self, **kwargs):
for vec in [self.Q0_vec, self.h_vec, self.k_vec]:
for child in vec.children:
child.observe(self._on_setting_changed, names="value")
+ child.observe(self._on_vector_changed, names="value")
self.Q0_widget = ipw.HBox(
[ipw.HTML("Q0: ", layout={"width": "20px"}), self.Q0_vec]
@@ -282,13 +272,16 @@ def __init__(self, **kwargs):
[ipw.HTML("k: ", layout={"width": "20px"}), self.k_vec]
)
- self.float_energy_broadening = ipw.FloatText(
+ self.energy_broadening = ipw.FloatText(
value=0.5,
description="ΔE (meV)",
tooltip="Energy window in eV",
)
-
- self.float_energy_broadening.observe(self._on_setting_changed, names="value")
+ ipw.link(
+ (self._model, "energy_broadening"),
+ (self.energy_broadening, "value"),
+ )
+ self.energy_broadening.observe(self._on_setting_changed, names="value")
self.plot_button.disabled = False
self.plot_button.description = "Plot"
@@ -310,10 +303,10 @@ def __init__(self, **kwargs):
]
),
# self.specification_intensity,
- self.float_ecenter,
- self.float_energy_broadening,
+ self.ecenter,
+ self.energy_broadening,
# self.int_energy_bins, # does not change anything here.
- self.float_T,
+ self.temperature,
self.weight_button,
],
layout=ipw.Layout(
@@ -335,13 +328,13 @@ def __init__(self, **kwargs):
),
]
- def _reset_settings(self, _):
- ####
- super()._reset_settings(_)
- self.h_vec.children[-2].value = 100 # n_h
- self.k_vec.children[-2].value = 100 # n_h
- self.h_vec.children[-1].value = 1 # alpha, or h_extension
- self.k_vec.children[-1].value = 1 # beta, or k_extension
+ def _on_vector_changed(self, change=None):
+ """
+ Update the model.
+ """
+ self._model.Q0_vec = [i.value for i in self.Q0_vec.children[:-2]]
+ self._model.h_vec = [i.value for i in self.h_vec.children]
+ self._model.k_vec = [i.value for i in self.k_vec.children]
class QSectionFullWidget(ipw.VBox):
@@ -354,94 +347,50 @@ class QSectionFullWidget(ipw.VBox):
and are from Sears (1992) Neutron News 3(3) pp26--37.
"""
- def __init__(self, fc, intensity_ref_0K=1, **kwargs):
- self.fc = fc
+ def __init__(self, model):
+ self._model = model
+ self.rendered = False
+ super().__init__()
+
+ def render(self):
+ if self.rendered:
+ return
self.title_intensity = ipw.HTML(
"Neutron dynamic structure factor in a Q-section visualization
"
)
- self.settings_intensity = QSectionSettingsWidget()
+ self.settings_intensity = QSectionSettingsWidget(model=self._model)
+
+ # rendering the widget.
+ self.settings_intensity.render()
+
+ # specific for the q section
self.settings_intensity.plot_button.on_click(self._on_plot_button_clicked)
self.settings_intensity.download_button.on_click(self.download_data)
- # This is used in order to have an overall intensity scale. Inherithed from the SingleCrystal
- self.intensity_ref_0K = intensity_ref_0K # CHANGED
+ self.children = [
+ self.title_intensity,
+ # self.map_widget,
+ self.settings_intensity,
+ ]
- super().__init__(
- children=[
- self.title_intensity,
- # self.map_widget,
- self.settings_intensity,
- ],
- )
+ self.rendered = True
def _on_plot_button_clicked(self, change=None):
- self.parameters = {
- "h": np.array(
- [i.value for i in self.settings_intensity.h_vec.children[:-2]]
- ),
- "k": np.array(
- [i.value for i in self.settings_intensity.k_vec.children[:-2]]
- ),
- "n_h": self.settings_intensity.h_vec.children[-2].value,
- "n_k": self.settings_intensity.k_vec.children[-2].value,
- "h_extension": self.settings_intensity.h_vec.children[-1].value,
- "k_extension": self.settings_intensity.k_vec.children[-1].value,
- "Q0": np.array(
- [i.value for i in self.settings_intensity.Q0_vec.children[:-2]]
- ),
- "ecenter": self.settings_intensity.float_ecenter.value,
- "deltaE": self.settings_intensity.float_energy_broadening.value,
- "bins": self.settings_intensity.int_energy_bins.value,
- "spectrum_type": self.settings_intensity.weight_button.value,
- "temperature": self.settings_intensity.float_T.value,
- }
-
- parameters_ = AttrDict(self.parameters) # CHANGED
-
- modes, q_array, h_array, k_array, labels, dw = produce_Q_section_modes(
- self.fc,
- h=parameters_.h,
- k=parameters_.k,
- Q0=parameters_.Q0,
- n_h=parameters_.n_h,
- n_k=parameters_.n_k,
- h_extension=parameters_.h_extension,
- k_extension=parameters_.k_extension,
- temperature=parameters_.temperature,
- )
+ self._model._update_qsection_spectra()
- av_spec, q_array, h_array, k_array, labels = produce_Q_section_spectrum(
- modes,
- q_array,
- h_array,
- k_array,
- ecenter=parameters_.ecenter,
- deltaE=parameters_.deltaE,
- bins=parameters_.bins,
- spectrum_type=parameters_.spectrum_type,
- dw=dw,
- labels=labels,
- )
-
- self.settings_intensity.plot_button.disabled = True
- self.settings_intensity.plot_button.description = "Replot"
-
- if hasattr(self, "map_widget"):
- self.map_widget._update_spectra(
- h_array, k_array, av_spec, labels
- ) # CHANGED
- else:
- self.map_widget = QSectionPlotWidget(
- h_array, k_array, av_spec, labels
- ) # CHANGED
+ if not hasattr(self, "map_widget"):
+ self.map_widget = QSectionPlotWidget(self._model) # CHANGED
+ self.map_widget.render()
self.children = [
self.title_intensity,
self.map_widget,
self.settings_intensity,
]
+ self.map_widget._update_plot()
+
def download_data(self, _=None):
"""
Download both the ForceConstants and the spectra json files.
@@ -465,9 +414,9 @@ def download_data(self, _=None):
[i.value for i in self.settings_intensity.Q0_vec.children[:-2]]
),
"weight": self.settings_intensity.weight_button.value,
- "ecenter": self.settings_intensity.float_ecenter.value,
- "deltaE": self.settings_intensity.float_energy_broadening.value,
- "temperature": self.settings_intensity.float_T.value,
+ "ecenter": self.settings_intensity.ecenter.value,
+ "deltaE": self.settings_intensity.energy_broadening.value,
+ "temperature": self.settings_intensity.temperature.value,
}
)
for k in ["h", "k", "Q0", "weight", "ecenter", "deltaE", "temperature"]:
diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_single_crystal_widgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_single_crystal_widgets.py
similarity index 51%
rename from src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_single_crystal_widgets.py
rename to src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_single_crystal_widgets.py
index f03df86..47e7e85 100644
--- a/src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_single_crystal_widgets.py
+++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_single_crystal_widgets.py
@@ -1,25 +1,18 @@
import base64
from IPython.display import display
-import numpy as np
-import copy
import ipywidgets as ipw
import plotly.graph_objects as go
import plotly.io as pio
# from ..euphonic.bands_pdos import *
-from ..euphonic.intensity_maps import (
- AttrDict,
- produce_bands_weigthed_data,
- generated_curated_data,
-)
import json
from monty.json import jsanitize
# sys and os used to prevent euphonic to print in the stdout.
-from aiidalab_qe_vibroscopy.utils.euphonic.euphonic_base_widgets import (
+from aiidalab_qe_vibroscopy.utils.euphonic.base_widgets.euphonic_base_widgets import (
StructureFactorSettingsBaseWidget,
COLORSCALE,
COLORBAR_DICT,
@@ -28,23 +21,15 @@
class SingleCrystalPlotWidget(StructureFactorBasePlotWidget):
- def __init__(self, spectra, intensity_ref_0K=1, **kwargs):
- (
- final_xspectra,
- final_zspectra,
- ticks_positions,
- ticks_labels,
- ) = generated_curated_data(spectra)
- # Data to contour is the sum of two Gaussian functions.
- x, y = np.meshgrid(spectra[0].x_data.magnitude, spectra[0].y_data.magnitude)
-
- self.intensity_ref_0K = intensity_ref_0K
+ def render(self):
+ if self.rendered:
+ return
- self.fig = go.FigureWidget()
+ super().render()
heatmap_trace = go.Heatmap(
- z=final_zspectra.T,
- y=y[:, 0] * self.THz_to_meV,
+ z=self._model.final_zspectra.T,
+ y=self._model.y[:, 0] * self.THz_to_meV,
x=None,
colorbar=COLORBAR_DICT,
colorscale=COLORSCALE, # imported from euphonic_base_widgets
@@ -58,45 +43,50 @@ def __init__(self, spectra, intensity_ref_0K=1, **kwargs):
# Add heatmap trace to figure
self.fig.add_trace(heatmap_trace)
+ # Layout settings
self.fig.update_layout(
xaxis=dict(
- tickmode="array", tickvals=ticks_positions, ticktext=ticks_labels
+ tickmode="array",
+ tickvals=self._model.ticks_positions,
+ ticktext=self._model.ticks_labels,
)
)
- self.fig["layout"]["yaxis"].update(range=[min(y[:, 0]), max(y[:, 0])])
- # Create and show figure
- super().__init__(
- final_xspectra,
- **kwargs,
+ self.fig["layout"]["yaxis"].update(
+ title="meV",
+ range=[min(self._model.y[:, 0]), max(self._model.y[:, 0])],
+ )
+
+ if self.fig.layout.images:
+ for image in self.fig.layout.images:
+ image["scl"] = 2 # Set the scale for each image
+
+ self.fig.update_layout(
+ height=500,
+ width=700,
+ margin=dict(l=15, r=15, t=15, b=15),
)
+ # Update x-axis and y-axis to enable autoscaling
+ self.fig.update_xaxes(autorange=True)
+ self.fig.update_yaxes(autorange=True)
- def _update_spectra(self, spectra):
- (
- final_xspectra,
- final_zspectra,
- ticks_positions,
- ticks_labels,
- ) = generated_curated_data(spectra)
- # Data to contour is the sum of two Gaussian functions.
- x, y = np.meshgrid(spectra[0].x_data.magnitude, spectra[0].y_data.magnitude)
-
- # If I do this
- # self.data = ()
- # I have a delay in the plotting, I have blank plot while it
- # is adding the new trace (see below); So, I will instead do the
- # re-assignement of the self.data = [self.data[1]] afterwards.
+ # Update the layout to enable autoscaling
+ self.fig.update_layout(autosize=True)
+
+ def _update_plot(self):
+ # update the spectra, i.e. the data contained in the _model.
+ self._model._update_spectra()
x = None # if mode == "intensity" else x[0]
self.fig.add_trace(
go.Heatmap(
- z=final_zspectra.T,
+ z=self._model.final_zspectra.T,
y=(
- y[:, 0] * self.THz_to_meV
+ self._model.y[:, 0] * self.THz_to_meV
if self.E_units_button.value == "meV"
- else y[:, 0]
+ else self._model.y[:, 0]
),
- x=x,
+ x=x, # self._model.x,
colorbar=COLORBAR_DICT,
colorscale=COLORSCALE, # imported from euphonic_base_widgets
)
@@ -106,17 +96,24 @@ def _update_spectra(self, spectra):
# this is delays things
self.fig.update_layout(
xaxis=dict(
- tickmode="array", tickvals=ticks_positions, ticktext=ticks_labels
+ tickmode="array",
+ tickvals=self._model.ticks_positions,
+ ticktext=self._model.ticks_labels,
)
)
self.fig.data = [self.fig.data[1]]
- super()._update_spectra(final_zspectra)
+ super()._update_plot()
class SingleCrystalSettingsWidget(StructureFactorSettingsBaseWidget):
- def __init__(self, **kwargs):
+ def render(self):
+ if self.rendered:
+ return
+
+ super().render()
+
self.custom_kpath_description = ipw.HTML(
"""
@@ -124,7 +121,7 @@ def __init__(self, **kwargs):
we can provide it via a specific format:
(1) each linear path should be divided by '|';
(2) each path is composed of 'qxi qyi qzi - qxf qyf qzf' where qxi and qxf are, respectively,
- the start and end x-coordinate of the q direction, in crystal coordinates.
+ the start and end x-coordinate of the q direction, in reciprocal lattice units (rlu).
An example path is: '0 0 0 - 1 1 1 | 1 1 1 - 0.5 0.5 0.5'.
For now, we do not support fractions (i.e. we accept 0.5 but not 1/2).
@@ -139,14 +136,12 @@ def __init__(self, **kwargs):
custom_style = ''
display(ipw.HTML(custom_style))
self.custom_kpath_text.add_class("custom-font")
-
+ ipw.link(
+ (self._model, "custom_kpath"),
+ (self.custom_kpath_text, "value"),
+ )
self.custom_kpath_text.observe(self._on_setting_changed, names="value")
- # Please note: if you change the order of the widgets below, it will
- # affect the usage of the children[0] below in the full widget.
-
- super().__init__()
-
self.children = [
ipw.HBox(
[
@@ -159,10 +154,10 @@ def __init__(self, **kwargs):
self.download_button,
]
),
- self.float_q_spacing,
- self.float_energy_broadening,
- self.int_energy_bins,
- self.float_T,
+ self.q_spacing,
+ self.energy_broadening,
+ self.energy_bins,
+ self.temperature,
self.weight_button,
],
layout=ipw.Layout(
@@ -182,108 +177,73 @@ def __init__(self, **kwargs):
),
]
- def _reset_settings(self, _):
- self.custom_kpath_text.value = ""
- super()._reset_settings(_)
-
class SingleCrystalFullWidget(ipw.VBox):
+ # I need to put the model also HERE! Think how we can so all of this in simpler way.
"""
The Widget to display specifically the Intensity map of Dynamical structure
- factor for single crystal.
+ factor for single crystal. It is composed of the following widgets:
+ - title_intensity: HTML widget with the title of the widget.
+ - settings_intensity: SingleCrystalSettingsWidget widget with the settings for the plot.
+ - map_widget: SingleCrystalPlotWidget widget with the plot of the intensity map.
+ - download_button: Button widget to download the intensity map.
+
The scattering lengths used in the `produce_bands_weigthed_data` function
are tabulated (Euphonic/euphonic/data/sears-1992.json)
and are from Sears (1992) Neutron News 3(3) pp26--37.
"""
- def __init__(self, fc, q_path, **kwargs):
- self.fc = fc
- self.q_path = q_path
+ def __init__(self, model):
+ self._model = model
+ self.rendered = False
+ super().__init__()
- self.spectra, self.parameters = produce_bands_weigthed_data(
- fc=self.fc,
- linear_path=self.q_path,
- plot=False, # CHANGED
- )
+ def render(self):
+ if self.rendered:
+ return
self.title_intensity = ipw.HTML(
"Neutron dynamic structure factor - Single Crystal
"
)
- self.settings_intensity = SingleCrystalSettingsWidget()
+ # we initialize and inject the model here.
+ self.settings_intensity = SingleCrystalSettingsWidget(model=self._model)
+ self.map_widget = SingleCrystalPlotWidget(model=self._model)
+
+ # rendering the widgets
+ self.settings_intensity.render()
+ self.map_widget.render()
+
+ # specific for the single crystal
self.settings_intensity.plot_button.on_click(self._on_plot_button_clicked)
self.settings_intensity.download_button.on_click(self.download_data)
- # This is used in order to have an overall intensity scale.
- self.intensity_ref_0K = np.max(self.spectra[0].z_data.magnitude) # CHANGED
-
- self.map_widget = SingleCrystalPlotWidget(
- self.spectra, intensity_ref_0K=self.intensity_ref_0K
- ) # CHANGED
+ self.children = [
+ self.title_intensity,
+ self.map_widget,
+ self.settings_intensity,
+ ]
- super().__init__(
- children=[
- self.title_intensity,
- self.map_widget,
- self.settings_intensity,
- ],
- )
+ self.rendered = True
def _on_plot_button_clicked(self, change=None):
- self.parameters.update(
- {
- "weighting": self.settings_intensity.weight_button.value,
- "q_spacing": self.settings_intensity.float_q_spacing.value,
- "energy_broadening": self.settings_intensity.float_energy_broadening.value,
- "ebins": self.settings_intensity.int_energy_bins.value,
- "temperature": self.settings_intensity.float_T.value,
- }
- )
- parameters_ = AttrDict(self.parameters) # CHANGED
-
- # custom linear path
- if len(self.settings_intensity.custom_kpath_text.value) > 1:
- coordinates, labels = self.curate_path_and_labels()
- linear_path = {
- "coordinates": coordinates,
- "labels": labels, # ["$\Gamma$","X","X","(1,1,1)"],
- "delta_q": parameters_["q_spacing"],
- }
- else:
- linear_path = copy.deepcopy(self.q_path)
- if linear_path:
- linear_path["delta_q"] = parameters_["q_spacing"]
-
- self.spectra, self.parameters = produce_bands_weigthed_data(
- params=parameters_,
- fc=self.fc,
- plot=False,
- linear_path=linear_path, # CHANGED
- )
-
self.settings_intensity.plot_button.disabled = True
- self.map_widget._update_spectra(self.spectra) # CHANGED
+ self.map_widget._update_plot()
def download_data(self, _=None):
"""
Download both the ForceConstants and the spectra json files.
+ TODO: improve the format, should be easy to open somewhere else.
"""
force_constants_dict = self.fc.to_dict()
filename = "single_crystal.json"
my_dict = {}
- for branch in range(len(self.spectra)):
- my_dict[str(branch)] = self.spectra[branch].to_dict()
- my_dict.update(
- {
- "weighting": self.settings_intensity.weight_button.value,
- "q_spacing": self.settings_intensity.float_q_spacing.value,
- "energy_broadening": self.settings_intensity.float_energy_broadening.value,
- "ebins": self.settings_intensity.int_energy_bins.value,
- "temperature": self.settings_intensity.float_T.value,
- }
- )
+ my_dict["x"] = self._model.final_xspectra.tolist()
+ my_dict["y"] = self._model.y.tolist()
+ my_dict["z"] = self._model.final_zspectra.tolist()
+ my_dict.update(self._model.get_model_state())
for k in ["weighting", "q_spacing", "temperature"]:
filename += "_" + k + "_" + str(my_dict[k])
@@ -305,27 +265,6 @@ def download_data(self, _=None):
b64_str = base64.b64encode(image_bytes).decode()
self._download(payload=b64_str, filename=filename + ".png")
- def curate_path_and_labels(
- self,
- ):
- # I do not like this implementation (MB)
- coordinates = []
- labels = []
- path = self.settings_intensity.custom_kpath_text.value
- linear_paths = path.split("|")
- for i in linear_paths:
- scoords = []
- s = i.split(
- " - "
- ) # not i.split("-"), otherwise also the minus of the negative numbers are used for the splitting.
- for k in s:
- labels.append(k.strip())
- # AAA missing support for fractions.
- l = tuple(map(float, [kk for kk in k.strip().split(" ")])) # noqa: E741
- scoords.append(l)
- coordinates.append(scoords)
- return coordinates, labels
-
@staticmethod
def _download(payload, filename):
from IPython.display import Javascript
diff --git a/src/aiidalab_qe_vibroscopy/utils/phonons/result.py b/src/aiidalab_qe_vibroscopy/utils/phonons/result.py
index 7b1296c..49a2759 100644
--- a/src/aiidalab_qe_vibroscopy/utils/phonons/result.py
+++ b/src/aiidalab_qe_vibroscopy/utils/phonons/result.py
@@ -1,6 +1,6 @@
"""Bands results view widgets"""
-from aiidalab_qe.common.bandpdoswidget import cmap, get_bands_labeling
+from aiidalab_qe.common.bands_pdos.utils import _cmap, _get_bands_labeling
import numpy as np
import json
@@ -34,25 +34,22 @@ def export_phononworkchain_data(node, fermi_energy=None):
}
parameters = {}
- if "vibronic" not in node.outputs:
- return None
-
- if "phonon_bands" in node.outputs.vibronic:
+ if "phonon_bands" in node.outputs:
"""
copied and pasted from aiidalab_qe.common.bandsplotwidget.
adapted for phonon outputs
"""
data = json.loads(
- node.outputs.vibronic.phonon_bands._exportcontent("json", comments=False)[0]
+ node.outputs.phonon_bands._exportcontent("json", comments=False)[0]
)
# The fermi energy from band calculation is not robust.
data["fermi_energy"] = 0
- data["pathlabels"] = get_bands_labeling(data)
+ data["pathlabels"] = _get_bands_labeling(data)
replace_symbols_with_uppercase(data["pathlabels"])
data["Y_label"] = "Dispersion (THz)"
- bands = node.outputs.vibronic.phonon_bands._get_bandplot_data(
+ bands = node.outputs.phonon_bands._get_bandplot_data(
cartesian=True, prettify_format=None, join_symbol=None, get_segments=True
)
parameters["energy_range"] = {
@@ -64,8 +61,8 @@ def export_phononworkchain_data(node, fermi_energy=None):
data["y"] = bands["y"]
full_data["bands"] = [data, parameters]
- if "phonon_pdos" in node.outputs.vibronic:
- phonopy_calc = node.outputs.vibronic.phonon_pdos.creator
+ if "phonon_pdos" in node.outputs:
+ phonopy_calc = node.outputs.phonon_pdos.creator
kwargs = {}
if "settings" in phonopy_calc.inputs:
@@ -75,7 +72,7 @@ def export_phononworkchain_data(node, fermi_energy=None):
kwargs.update({key: the_settings[key]})
symbols = node.inputs.structure.get_ase().get_chemical_symbols()
- pdos = node.outputs.vibronic.phonon_pdos
+ pdos = node.outputs.phonon_pdos
index_dict, dos_dict = (
{},
@@ -119,8 +116,8 @@ def export_phononworkchain_data(node, fermi_energy=None):
"label": atom,
"x": pdos.get_x()[1].tolist(),
"y": dos_dict[atom].tolist(),
- "borderColor": cmap(atom),
- "backgroundColor": cmap(atom),
+ "borderColor": _cmap(atom),
+ "backgroundColor": _cmap(atom),
"backgroundAlpha": "40%",
"lineStyle": "solid",
}
@@ -140,27 +137,27 @@ def export_phononworkchain_data(node, fermi_energy=None):
full_data["pdos"] = [json.loads(json.dumps(data_dict)), parameters, "dos"]
- if "phonon_thermo" in node.outputs.vibronic:
+ if "phonon_thermo" in node.outputs:
(
what,
T,
units_k,
- ) = node.outputs.vibronic.phonon_thermo.get_x()
+ ) = node.outputs.phonon_thermo.get_x()
(
F_name,
F_data,
units_F,
- ) = node.outputs.vibronic.phonon_thermo.get_y()[0]
+ ) = node.outputs.phonon_thermo.get_y()[0]
(
Entropy_name,
Entropy_data,
units_entropy,
- ) = node.outputs.vibronic.phonon_thermo.get_y()[1]
+ ) = node.outputs.phonon_thermo.get_y()[1]
(
Cv_name,
Cv_data,
units_Cv,
- ) = node.outputs.vibronic.phonon_thermo.get_y()[2]
+ ) = node.outputs.phonon_thermo.get_y()[2]
full_data["thermo"] = (
[T, F_data, units_F, Entropy_data, units_entropy, Cv_data, units_Cv],
diff --git a/src/aiidalab_qe_vibroscopy/utils/raman/result.py b/src/aiidalab_qe_vibroscopy/utils/raman/result.py
index cb8287d..bc488e6 100644
--- a/src/aiidalab_qe_vibroscopy/utils/raman/result.py
+++ b/src/aiidalab_qe_vibroscopy/utils/raman/result.py
@@ -3,13 +3,8 @@
from __future__ import annotations
-import ipywidgets as ipw
import numpy as np
-from IPython.display import HTML, clear_output, display
-import base64
-import json
-from weas_widget import WeasWidget
from aiida_vibroscopy.utils.broadenings import multilorentz
@@ -43,16 +38,13 @@ def export_iramanworkchain_data(node):
We have multiple choices: IR, RAMAN.
"""
- if "vibronic" not in node.outputs:
- return None
+ if "iraman" in node:
+ output_node = node.iraman
+ elif "harmonic" in node:
+ output_node = node.harmonic
else:
- if "iraman" in node.outputs.vibronic:
- output_node = node.outputs.vibronic.iraman
- elif "harmonic" in node.outputs.vibronic:
- output_node = node.outputs.vibronic.harmonic
- else:
- # we have raman and ir only if we run IRamanWorkChain or HarmonicWorkChain
- return None
+ # we have raman and ir only if we run IRamanWorkChain or HarmonicWorkChain
+ return None
if "vibrational_data" in output_node:
# We enable the possibility to provide both spectra.
@@ -106,490 +98,3 @@ def export_iramanworkchain_data(node):
return spectra_data
else:
return None
-
-
-class SpectrumPlotWidget(ipw.VBox):
- """Widget that allows different options for plotting Raman or IR Spectrum."""
-
- def __init__(self, node, output_node, spectrum_type, **kwargs):
- self.node = node
- self.output_node = output_node
- self.spectrum_type = spectrum_type
-
- # VibrationalData
- vibrational_data = self.output_node.vibrational_data
- self.vibro = (
- vibrational_data.numerical_accuracy_4
- if hasattr(vibrational_data, "numerical_accuracy_4")
- else vibrational_data.numerical_accuracy_2
- )
-
- self.description = ipw.HTML(
- f"""
- Select the type of {self.spectrum_type} spectrum to plot.
-
"""
- )
-
- self._plot_type = ipw.ToggleButtons(
- options=[
- ("Powder", "powder"),
- ("Single Crystal", "single_crystal"),
- ],
- value="powder",
- description="Spectrum type:",
- disabled=False,
- style={"description_width": "initial"},
- )
- self.temperature = ipw.FloatText(
- value=298.0,
- description="Temperature (K):",
- disabled=False,
- style={"description_width": "initial"},
- )
- self.frequency_laser = ipw.FloatText(
- value=532.0,
- description="Laser frequency (nm):",
- disabled=False,
- style={"description_width": "initial"},
- )
- self.pol_incoming = ipw.Text(
- value="0 0 1",
- description="Incoming polarization:",
- disabled=False,
- style={"description_width": "initial"},
- )
- self.pol_outgoing = ipw.Text(
- value="0 0 1",
- description="Outgoing polarization:",
- disabled=False,
- style={"description_width": "initial"},
- )
- self.plot_button = ipw.Button(
- description="Plot",
- icon="pencil",
- button_style="primary",
- disabled=False,
- layout=ipw.Layout(width="auto"),
- )
- self.download_button = ipw.Button(
- description="Download Data",
- icon="download",
- button_style="primary",
- disabled=False,
- layout=ipw.Layout(width="auto", visibility="hidden"),
- )
- self.wrong_syntax = ipw.HTML(
- value=""" wrong syntax""",
- layout={"visibility": "hidden"},
- )
- self.broadening = ipw.FloatText(
- value=10.0,
- description="Broadening (cm-1):",
- disabled=False,
- style={"description_width": "initial"},
- )
- self.spectrum_widget = ipw.Output()
- self.frequencies = []
- self.intensities = []
- self.polarization_out = ipw.Output()
-
- # Polarized and Unpolirized intensities
- self.separate_polarized_display = ipw.Output()
- self.separate_polarized = ipw.Checkbox(
- value=False,
- description="Separate polarized and depolarized intensities",
- disabled=False,
- style={"description_width": "initial"},
- )
-
- def download_data(_=None):
- filename = "spectra.json"
- if self.separate_polarized.value:
- my_dict = {
- "Frequencies cm-1": self.frequencies.tolist(),
- "Polarized intensities": self.intensities.tolist(),
- "Depolarized intensities": self.intensities_depolarized.tolist(),
- }
- else:
- my_dict = {
- "Frequencies cm-1": self.frequencies.tolist(),
- "Intensities": self.intensities.tolist(),
- }
- json_str = json.dumps(my_dict)
- b64_str = base64.b64encode(json_str.encode()).decode()
- self._download(payload=b64_str, filename=filename)
-
- # hide the outgoing pol, frequency and temperature if ir:
- if self.spectrum_type == "IR":
- self.temperature.layout.display = "none"
- self.frequency_laser.layout.display = "none"
- self.pol_outgoing.layout.display == "none"
-
- self._plot_type.observe(self._on_plot_type_change, names="value")
- self.plot_button.on_click(self._on_plot_button_clicked)
- self.download_button.on_click(download_data)
- super().__init__(
- children=[
- self.description,
- self._plot_type,
- self.temperature,
- self.frequency_laser,
- self.broadening,
- self.separate_polarized_display,
- self.polarization_out,
- ipw.HBox([self.plot_button, self.download_button]),
- self.wrong_syntax,
- self.spectrum_widget,
- ]
- )
- self._update_separate_polarized()
-
- @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)
-
- def _on_plot_type_change(self, change):
- if change["new"] == "single_crystal":
- with self.polarization_out:
- clear_output()
- display(self.pol_incoming)
- if self.spectrum_type == "Raman":
- display(self.pol_outgoing)
- else:
- self.pol_incoming.value = "0 0 1"
- self.pol_outgoing.value = "0 0 1"
- self.wrong_syntax.layout.visibility = "hidden"
- with self.polarization_out:
- clear_output()
-
- self._update_separate_polarized()
-
- def _update_separate_polarized(self):
- with self.separate_polarized_display:
- clear_output()
- if (self._plot_type.value == "powder") and (self.spectrum_type == "Raman"):
- display(self.separate_polarized)
- else:
- self.separate_polarized.value = False
-
- def _on_plot_button_clicked(self, change):
- if self._plot_type.value == "powder":
- # Powder spectrum
- if self.spectrum_type == "Raman":
- (
- polarized_intensities,
- depolarized_intensities,
- frequencies,
- labels,
- ) = self.vibro.run_powder_raman_intensities(
- frequencies=self.frequency_laser.value,
- temperature=self.temperature.value,
- )
- total_intensities = polarized_intensities + depolarized_intensities
- else:
- (
- polarized_intensities,
- frequencies,
- labels,
- ) = self.vibro.run_powder_ir_intensities()
- total_intensities = polarized_intensities
- if self.separate_polarized.value:
- self.frequencies, self.intensities = plot_powder(
- frequencies,
- polarized_intensities,
- self.broadening.value,
- )
- self.frequencies_depolarized, self.intensities_depolarized = (
- plot_powder(
- frequencies,
- depolarized_intensities,
- self.broadening.value,
- )
- )
- self._display_figure()
- else:
- self.frequencies, self.intensities = plot_powder(
- frequencies,
- total_intensities,
- self.broadening.value,
- )
- self._display_figure()
-
- else:
- # Single crystal spectrum
- dir_incoming, correct_syntax_incoming = self._check_inputs_correct(
- self.pol_incoming
- )
- dir_outgoing, correct_syntax_outgoing = self._check_inputs_correct(
- self.pol_outgoing
- )
- if not correct_syntax_incoming or not correct_syntax_outgoing:
- self.wrong_syntax.layout.visibility = "visible"
- return
- else:
- self.wrong_syntax.layout.visibility = "hidden"
- if self.spectrum_type == "Raman":
- (
- intensities,
- frequencies,
- labels,
- ) = self.vibro.run_single_crystal_raman_intensities(
- pol_incoming=dir_incoming,
- pol_outgoing=dir_incoming,
- frequencies=self.frequency_laser.value,
- temperature=self.temperature.value,
- )
- else:
- (
- intensities,
- frequencies,
- labels,
- ) = self.vibro.run_single_crystal_ir_intensities(
- pol_incoming=dir_incoming
- )
-
- self.frequencies, self.intensities = plot_powder(
- frequencies, intensities
- )
- self._display_figure()
-
- def _check_inputs_correct(self, polarization):
- # Check if the polarization vectors are correct
- input_text = polarization.value
- input_values = input_text.split()
- dir_values = []
- if len(input_values) == 3:
- try:
- dir_values = [float(i) for i in input_values]
- return dir_values, True
- except: # noqa: E722
- return dir_values, False
- else:
- return dir_values, False
-
- def _spectrum_widget(self):
- import plotly.graph_objects as go
-
- fig = go.FigureWidget(
- layout=go.Layout(
- title=dict(text=f"{self.spectrum_type} spectrum"),
- barmode="overlay",
- )
- )
- fig.layout.xaxis.title = "Wavenumber (cm-1)"
- fig.layout.yaxis.title = "Intensity (arb. units)"
- fig.layout.xaxis.nticks = 0
- if self.separate_polarized.value:
- fig.add_scatter(x=self.frequencies, y=self.intensities, name="Polarized")
- fig.add_scatter(
- x=self.frequencies_depolarized,
- y=self.intensities_depolarized,
- name="Depolarized",
- )
- else:
- fig.add_scatter(x=self.frequencies, y=self.intensities, name="")
- fig.update_layout(
- height=500,
- width=700,
- plot_bgcolor="white",
- )
- return fig
-
- def _display_figure(self):
- with self.spectrum_widget:
- clear_output()
- display(self._spectrum_widget())
- self.download_button.layout.visibility = "visible"
-
-
-class ActiveModesWidget(ipw.VBox):
- """Widget that display an animation (nglview) of the active modes."""
-
- def __init__(self, node, output_node, spectrum_type, **kwargs):
- self.node = node
- self.output_node = output_node
- self.spectrum_type = spectrum_type
-
- # WeasWidget configuration
- self.guiConfig = {
- "enabled": True,
- "components": {
- "atomsControl": True,
- "buttons": True,
- "cameraControls": True,
- },
- "buttons": {
- "fullscreen": True,
- "download": True,
- "measurement": True,
- },
- }
- # VibrationalData
- vibrational_data = self.output_node.vibrational_data
- self.vibro = (
- vibrational_data.numerical_accuracy_4
- if hasattr(vibrational_data, "numerical_accuracy_4")
- else vibrational_data.numerical_accuracy_2
- )
-
- # Raman or IR active modes
- selection_rule = self.spectrum_type.lower()
- frequencies, self.eigenvectors, self.labels = self.vibro.run_active_modes(
- selection_rule=selection_rule,
- )
- self.rounded_frequencies = [round(frequency, 3) for frequency in frequencies]
-
- # StructureData
- self.structure_ase = self.node.inputs.structure.get_ase()
-
- modes_values = [
- f"{index + 1}: {value}"
- for index, value in enumerate(self.rounded_frequencies)
- ]
- # Create Raman modes widget
- self.active_modes = ipw.Dropdown(
- options=modes_values,
- value=modes_values[0], # Default value
- description="Select mode:",
- style={"description_width": "initial"},
- )
-
- self.amplitude = ipw.FloatText(
- value=3.0,
- description="Amplitude :",
- disabled=False,
- style={"description_width": "initial"},
- )
-
- self._supercell = [
- ipw.BoundedIntText(value=1, min=1, layout={"width": "40px"}),
- ipw.BoundedIntText(value=1, min=1, layout={"width": "40px"}),
- ipw.BoundedIntText(value=1, min=1, layout={"width": "40px"}),
- ]
-
- self.supercell_selector = ipw.HBox(
- [
- ipw.HTML(
- description="Super cell:", style={"description_width": "initial"}
- )
- ]
- + self._supercell
- )
-
- self.modes_table = ipw.Output()
- self.animation = ipw.Output()
- self._display_table()
- self._select_active_mode(None)
- widget_list = [
- self.active_modes,
- self.amplitude,
- self._supercell[0],
- self._supercell[1],
- self._supercell[2],
- ]
- for elem in widget_list:
- elem.observe(self._select_active_mode, names="value")
-
- super().__init__(
- children=[
- ipw.HBox(
- [
- ipw.VBox(
- [
- ipw.HTML(
- value=f" {self.spectrum_type} Active Modes "
- ),
- self.modes_table,
- ]
- ),
- ipw.VBox(
- [
- self.active_modes,
- self.amplitude,
- self.supercell_selector,
- self.animation,
- ],
- layout={"justify_content": "center"},
- ),
- ]
- ),
- ]
- )
-
- def _display_table(self):
- """Display table with the active modes."""
- # Create an HTML table with the active modes
- table_data = [list(x) for x in zip(self.rounded_frequencies, self.labels)]
- table_html = ""
- table_html += "Frequencies (cm-1) | Label |
"
- for row in table_data:
- table_html += ""
- for cell in row:
- table_html += "{} | ".format(cell)
- table_html += "
"
- table_html += "
"
- # Set layout to a fix size
- self.modes_table.layout = {
- "overflow": "auto",
- "height": "200px",
- "width": "150px",
- }
- with self.modes_table:
- clear_output()
- display(HTML(table_html))
-
- def _select_active_mode(self, change):
- """Display animation of the selected active mode."""
- self._animation_widget()
- with self.animation:
- clear_output()
- display(self.weas)
-
- def _animation_widget(self):
- """Create animation widget."""
- # Get the index of the selected mode
- index_str = self.active_modes.value.split(":")[0]
- index = int(index_str) - 1
- # Get the eigenvector of the selected mode
- eigenvector = self.eigenvectors[index]
- # Get the amplitude of the selected mode
- amplitude = self.amplitude.value
- # Get the structure of the selected mode
- structure = self.structure_ase
-
- self.weas = WeasWidget(guiConfig=self.guiConfig)
- self.weas.from_ase(structure)
-
- phonon_setting = {
- "eigenvectors": np.array(
- [[[real_part, 0] for real_part in row] for row in eigenvector]
- ),
- "kpoint": [0, 0, 0], # optional
- "amplitude": amplitude,
- "factor": amplitude * 0.6,
- "nframes": 20,
- "repeat": [
- self._supercell[0].value,
- self._supercell[1].value,
- self._supercell[2].value,
- ],
- "color": "black",
- "radius": 0.1,
- }
- self.weas.avr.phonon_setting = phonon_setting
-
- self.weas.avr.model_style = 1
- self.weas.avr.color_type = "JMOL"
- self.weas.avr.vf.show = True