diff --git a/src/aiidalab_qe_vibroscopy/app/result/result.py b/src/aiidalab_qe_vibroscopy/app/result/result.py index 2ce571a..8184fc8 100644 --- a/src/aiidalab_qe_vibroscopy/app/result/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result/result.py @@ -13,12 +13,14 @@ 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.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]): 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..a014346 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py @@ -0,0 +1,163 @@ +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 +import numpy as np +from euphonic import ForceConstants + +from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import ( + produce_bands_weigthed_data, + generated_curated_data, +) + + +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