From 194c3b3aeeceb90a39cae252d495e909ca3011f3 Mon Sep 17 00:00:00 2001 From: AndresOrtegaGuerrero Date: Mon, 2 Dec 2024 10:22:14 +0000 Subject: [PATCH] setting IR/Raman Results tab --- pyproject.toml | 2 +- src/aiidalab_qe_vibroscopy/app/result.py | 46 ---- .../app/result/model.py | 16 +- .../app/result/result.py | 16 +- .../app/widgets/ir_ramanmodel.py | 26 ++ .../app/widgets/ir_ramanwidget.py | 58 +++++ .../app/widgets/ramanmodel.py | 206 +++++++++++----- .../app/widgets/ramanwidget.py | 230 ++++++++++++++---- .../utils/raman/result.py | 184 -------------- 9 files changed, 431 insertions(+), 353 deletions(-) create mode 100644 src/aiidalab_qe_vibroscopy/app/widgets/ir_ramanmodel.py create mode 100644 src/aiidalab_qe_vibroscopy/app/widgets/ir_ramanwidget.py diff --git a/pyproject.toml b/pyproject.toml index c8381cb..ce4f9a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "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/result.py b/src/aiidalab_qe_vibroscopy/app/result.py index d62b276..2f6f710 100644 --- a/src/aiidalab_qe_vibroscopy/app/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result.py @@ -8,7 +8,6 @@ from IPython.display import display import numpy as np -from ..utils.raman.result import export_iramanworkchain_data from ..utils.phonons.result import export_phononworkchain_data from ..utils.euphonic import ( @@ -19,8 +18,6 @@ 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): @@ -82,7 +79,6 @@ 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) @@ -164,48 +160,6 @@ def _update_view(self): ), ) # 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") - # euphonic if ins_data: intensity_maps = EuphonicSuperWidget( diff --git a/src/aiidalab_qe_vibroscopy/app/result/model.py b/src/aiidalab_qe_vibroscopy/app/result/model.py index d4862c3..a723014 100644 --- a/src/aiidalab_qe_vibroscopy/app/result/model.py +++ b/src/aiidalab_qe_vibroscopy/app/result/model.py @@ -2,10 +2,6 @@ import traitlets as tl -from aiidalab_qe_vibroscopy.utils.phonons.result import export_phononworkchain_data -from aiidalab_qe_vibroscopy.utils.euphonic import export_euphonic_data - - class VibroResultsModel(ResultsModel): identifier = "vibronic" @@ -30,8 +26,16 @@ def needs_raman_tab(self): # Here we use _fetch_child_process_node() since the function needs the input_structure in inputs def needs_phonons_tab(self): - return export_phononworkchain_data(self._fetch_child_process_node()) + 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): - return export_euphonic_data(self._fetch_child_process_node()) + 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 index ab24155..ba8001e 100644 --- a/src/aiidalab_qe_vibroscopy/app/result/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result/result.py @@ -7,8 +7,8 @@ from aiidalab_qe_vibroscopy.app.widgets.dielectricmodel import DielectricModel import ipywidgets as ipw -from aiidalab_qe_vibroscopy.app.widgets.ramanwidget import RamanWidget -from aiidalab_qe_vibroscopy.app.widgets.ramanmodel import RamanModel +from aiidalab_qe_vibroscopy.app.widgets.ir_ramanwidget import IRRamanWidget +from aiidalab_qe_vibroscopy.app.widgets.ir_ramanmodel import IRRamanModel class VibroResultsPanel(ResultsPanel[VibroResultsModel]): @@ -37,12 +37,16 @@ def render(self): needs_raman_tab = self._model.needs_raman_tab() if needs_raman_tab: - raman_model = RamanModel() - raman_widget = RamanWidget( - model=raman_model, + 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", raman_widget)) + + tab_data.append(("Raman/IR spectra", irraman_widget)) needs_dielectri_tab = self._model.needs_dielectric_tab() 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/ramanmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/ramanmodel.py index 46e9281..c1bebfa 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/ramanmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/ramanmodel.py @@ -2,6 +2,7 @@ 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 @@ -12,21 +13,24 @@ class RamanModel(Model): vibro = tl.Instance(AttributeDict, allow_none=True) + input_structure = tl.Instance(Atoms, allow_none=True) + spectrum_type = tl.Unicode() - raman_plot_type_options = tl.List( + plot_type_options = tl.List( trait=tl.List(tl.Unicode()), default_value=[ ("Powder", "powder"), ("Single Crystal", "single_crystal"), ], ) - raman_plot_type = tl.Unicode("powder") - raman_temperature = tl.Float(300) - raman_frequency_laser = tl.Float(532) - raman_pol_incoming = tl.Unicode("0 0 1") - raman_pol_outgoing = tl.Unicode("0 0 1") - raman_broadening = tl.Float(10.0) - raman_separate_polarizations = tl.Bool(False) + + 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 = [] @@ -34,72 +38,124 @@ class RamanModel(Model): 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 Raman plot data based on the selected plot type and configuration. + Update the plot data based on the selected spectrum type, plot type, and configuration. """ - if self.raman_plot_type == "powder": + 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 Raman plot. + Update data for the powder plot, handling both Raman and IR spectra. """ - ( - polarized_intensities, - depolarized_intensities, - frequencies, - _, - ) = self.raman_data.run_powder_raman_intensities( - frequencies=self.raman_frequency_laser, - temperature=self.raman_temperature, - ) - - if self.raman_separate_polarizations: - self.frequencies, self.intensities = self.generate_plot_data( - frequencies, + if self.spectrum_type == "Raman": + ( polarized_intensities, - self.raman_broadening, + depolarized_intensities, + frequencies, + _, + ) = self.raman_data.run_powder_raman_intensities( + frequencies=self.frequency_laser, + temperature=self.temperature, ) - self.frequencies_depolarized, self.intensities_depolarized = ( - self.generate_plot_data( + + if self.separate_polarizations: + self.frequencies, self.intensities = self.generate_plot_data( frequencies, - depolarized_intensities, - self.raman_broadening, + polarized_intensities, + self.broadening, ) - ) - else: - combined_intensities = polarized_intensities + depolarized_intensities + 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, - combined_intensities, - self.raman_broadening, + intensities, + self.broadening, ) self.frequencies_depolarized, self.intensities_depolarized = [], [] def _update_single_crystal_data(self): """ - Update data for the single crystal Raman plot. + Update data for the single crystal plot, handling both Raman and IR spectra. """ - dir_incoming, _ = self._check_inputs_correct(self.raman_pol_incoming) - dir_outgoing, _ = self._check_inputs_correct(self.raman_pol_outgoing) - - ( - intensities, - frequencies, - labels, - ) = self.raman_data.run_single_crystal_raman_intensities( - pol_incoming=dir_incoming, - pol_outgoing=dir_outgoing, - frequencies=self.raman_frequency_laser, - temperature=self.raman_temperature, - ) + 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 ) @@ -114,7 +170,7 @@ def update_plot(self, plot): """ update_function = ( self._update_powder_plot - if self.raman_plot_type == "powder" + if self.plot_type == "powder" else self._update_single_crystal_plot ) update_function(plot) @@ -126,7 +182,7 @@ def _update_powder_plot(self, plot): Parameters: plot: The plotly.graph_objs.Figure widget to update. """ - if self.raman_separate_polarizations: + if self.separate_polarizations: self._update_polarized_and_depolarized(plot) else: self._clear_depolarized_and_update(plot) @@ -149,7 +205,7 @@ def _update_polarized_and_depolarized(self, plot): name="Depolarized", ) ) - plot.layout.title.text = "Powder Raman Spectrum" + 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" @@ -160,7 +216,7 @@ def _update_polarized_and_depolarized(self, plot): self.intensities_depolarized, "Depolarized", ) - plot.layout.title.text = "Powder Raman Spectrum" + plot.layout.title.text = f"Powder {self.spectrum_type} Spectrum" def _clear_depolarized_and_update(self, plot): """ @@ -173,10 +229,10 @@ def _clear_depolarized_and_update(self, plot): self._update_trace(plot.data[0], self.frequencies, self.intensities, "") plot.data[1].x = [] plot.data[1].y = [] - plot.layout.title.text = "Powder Raman Spectrum" + 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 = "Powder Raman Spectrum" + plot.layout.title.text = f"Powder{self.spectrum_type} Spectrum" def _update_single_crystal_plot(self, plot): """ @@ -189,10 +245,10 @@ def _update_single_crystal_plot(self, plot): self._update_trace(plot.data[0], self.frequencies, self.intensities, "") plot.data[1].x = [] plot.data[1].y = [] - plot.layout.title.text = "Single Crystal Raman Spectrum" + 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 = "Single Crystal Raman Spectrum" + plot.layout.title.text = f"Single Crystal {self.spectrum_type} Spectrum" def _update_trace(self, trace, x_data, y_data, name): """ @@ -273,9 +329,45 @@ def generate_plot_data( 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 += "" + for row in table_data: + table_html += "" + for cell in row: + table_html += "".format(cell) + table_html += "" + table_html += "
Frequencies (cm-1) Label
{}
" + + 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.raman_separate_polarizations: + if self.separate_polarizations: my_dict = { "Frequencies cm-1": self.frequencies.tolist(), "Polarized intensities": self.intensities.tolist(), diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/ramanwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/ramanwidget.py index 085fa7e..503ead8 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/ramanwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/ramanwidget.py @@ -2,6 +2,8 @@ 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): @@ -9,100 +11,118 @@ class RamanWidget(ipw.VBox): Widget to display Raman properties Tab """ - def __init__(self, model: RamanModel, node: None, **kwargs): + 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.raman_plot_type = ipw.ToggleButtons( + 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, "raman_plot_type_options"), - (self.raman_plot_type, "options"), + (self._model, "plot_type_options"), + (self.plot_type, "options"), ) ipw.link( - (self._model, "raman_plot_type"), - (self.raman_plot_type, "value"), + (self._model, "plot_type"), + (self.plot_type, "value"), ) - self.raman_plot_type.observe(self._on_raman_plot_type_change, names="value") - self.raman_temperature = ipw.FloatText( + 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, "raman_temperature"), - (self.raman_temperature, "value"), + (self._model, "temperature"), + (self.temperature, "value"), ) - self.raman_frequency_laser = ipw.FloatText( + self.frequency_laser = ipw.FloatText( description="Laser frequency (nm):", style={"description_width": "initial"}, ) ipw.link( - (self._model, "raman_frequency_laser"), - (self.raman_frequency_laser, "value"), + (self._model, "frequency_laser"), + (self.frequency_laser, "value"), ) - self.raman_pol_incoming = ipw.Text( + self.pol_incoming = ipw.Text( description="Incoming polarization:", style={"description_width": "initial"}, layout=ipw.Layout(visibility="hidden"), ) ipw.link( - (self._model, "raman_pol_incoming"), - (self.raman_pol_incoming, "value"), + (self._model, "pol_incoming"), + (self.pol_incoming, "value"), ) - self.raman_pol_outgoing = ipw.Text( + self.pol_outgoing = ipw.Text( description="Outgoing polarization:", style={"description_width": "initial"}, layout=ipw.Layout(visibility="hidden"), ) ipw.link( - (self._model, "raman_pol_outgoing"), - (self.raman_pol_outgoing, "value"), + (self._model, "pol_outgoing"), + (self.pol_outgoing, "value"), ) - self.raman_plot_button = ipw.Button( + self.plot_button = ipw.Button( description="Update Plot", icon="pencil", button_style="primary", layout=ipw.Layout(width="auto"), ) - self.raman_plot_button.on_click(self._on_raman_plot_button_click) - self.raman_download_button = ipw.Button( + 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.raman_download_button.on_click(self._model.download_data) + self.download_button.on_click(self._model.download_data) self._wrong_syntax = StatusHTML(clear_after=8) - self.raman_broadening = ipw.FloatText( + self.broadening = ipw.FloatText( description="Broadening (cm-1):", style={"description_width": "initial"}, ) ipw.link( - (self._model, "raman_broadening"), - (self.raman_broadening, "value"), + (self._model, "broadening"), + (self.broadening, "value"), ) - self.raman_separate_polarized = ipw.Checkbox( + self.separate_polarizations = ipw.Checkbox( description="Separate polarized and depolarized intensities", style={"description_width": "initial"}, ) ipw.link( - (self._model, "raman_separate_polarizations"), - (self.raman_separate_polarized, "value"), + (self._model, "separate_polarizations"), + (self.separate_polarizations, "value"), ) - self.raman_spectrum = go.FigureWidget( + self.spectrum = go.FigureWidget( layout=go.Layout( title=dict(text="Powder Raman spectrum"), barmode="overlay", @@ -119,51 +139,149 @@ def render(self): ) ) + # 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("

Raman spectroscopy

"), + ipw.HTML(f"

{self._model.spectrum_type} spectroscopy

"), ipw.HTML( """
Select the type of Raman spectrum to plot.
""" ), - self.raman_plot_type, - self.raman_temperature, - self.raman_frequency_laser, - self.raman_broadening, - self.raman_separate_polarized, - self.raman_pol_incoming, - self.raman_pol_outgoing, - ipw.HBox([self.raman_plot_button, self.raman_download_button]), - self.raman_spectrum, - ipw.HTML("

IR spectroscopy

"), + 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() - self.raman_spectrum.add_scatter( + 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) - def _on_raman_plot_type_change(self, change): + 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.raman_pol_incoming.layout.visibility = "visible" - self.raman_pol_outgoing.layout.visibility = "visible" - self.raman_separate_polarized.layout.visibility = "hidden" + 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.raman_pol_incoming.layout.visibility = "hidden" - self.raman_pol_outgoing.layout.visibility = "hidden" - self.raman_separate_polarized.layout.visibility = "visible" + self.pol_incoming.layout.visibility = "hidden" + self.pol_outgoing.layout.visibility = "hidden" + self.separate_polarizations.layout.visibility = "visible" - def _on_raman_plot_button_click(self, _): + def _on_plot_button_click(self, _): _, incoming_syntax_ok = self._model._check_inputs_correct( - self.raman_pol_incoming.value + self.pol_incoming.value ) _, outgoing_syntax_ok = self._model._check_inputs_correct( - self.raman_pol_outgoing.value + self.pol_outgoing.value ) if not (incoming_syntax_ok and outgoing_syntax_ok): self._wrong_syntax.message = """ @@ -173,4 +291,10 @@ def _on_raman_plot_button_click(self, _): """ return self._model.update_data() - self._model.update_plot(self.raman_spectrum) + 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/utils/raman/result.py b/src/aiidalab_qe_vibroscopy/utils/raman/result.py index 4b59e17..bc488e6 100644 --- a/src/aiidalab_qe_vibroscopy/utils/raman/result.py +++ b/src/aiidalab_qe_vibroscopy/utils/raman/result.py @@ -3,11 +3,8 @@ from __future__ import annotations -import ipywidgets as ipw import numpy as np -from IPython.display import HTML, clear_output, display -from weas_widget import WeasWidget from aiida_vibroscopy.utils.broadenings import multilorentz @@ -101,184 +98,3 @@ def export_iramanworkchain_data(node): return spectra_data else: return None - - -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 += "" - for row in table_data: - table_html += "" - for cell in row: - table_html += "".format(cell) - table_html += "" - table_html += "
Frequencies (cm-1) Label
{}
" - # 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