From 7f2e0c0766281f951e9a7b6757e639e15b07e0e0 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Tue, 3 Dec 2024 17:49:52 +0000 Subject: [PATCH] Adding LL for euphonic. missing: download of the data, proper testing, detached app. --- .../app/result/result.py | 7 +- .../app/widgets/euphonicmodel.py | 261 ++++++++++---- .../app/widgets/euphonicwidget.py | 303 ++++++++++++++++ .../utils/euphonic/__init__.py | 336 ------------------ .../euphonic_base_widgets.py | 121 +++---- .../{ => data_manipulation}/bands_pdos.py | 0 .../{ => data_manipulation}/intensity_maps.py | 11 +- .../euphonic_powder_widgets.py | 201 +++++------ .../euphonic_q_planes_widgets.py | 205 ++++------- .../euphonic_single_crystal_widgets.py | 173 ++++----- 10 files changed, 790 insertions(+), 828 deletions(-) create mode 100644 src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py rename src/aiidalab_qe_vibroscopy/utils/euphonic/{ => base_widgets}/euphonic_base_widgets.py (74%) rename src/aiidalab_qe_vibroscopy/utils/euphonic/{ => data_manipulation}/bands_pdos.py (100%) rename src/aiidalab_qe_vibroscopy/utils/euphonic/{ => data_manipulation}/intensity_maps.py (98%) rename src/aiidalab_qe_vibroscopy/utils/euphonic/{ => tab_widgets}/euphonic_powder_widgets.py (51%) rename src/aiidalab_qe_vibroscopy/utils/euphonic/{ => tab_widgets}/euphonic_q_planes_widgets.py (70%) rename src/aiidalab_qe_vibroscopy/utils/euphonic/{ => tab_widgets}/euphonic_single_crystal_widgets.py (63%) diff --git a/src/aiidalab_qe_vibroscopy/app/result/result.py b/src/aiidalab_qe_vibroscopy/app/result/result.py index c3cc01b..3826fe3 100644 --- a/src/aiidalab_qe_vibroscopy/app/result/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result/result.py @@ -69,7 +69,12 @@ def render(self): tab_data.append(("Dielectric Properties", dielectric_widget)) if self._model.needs_euphonic_tab(): - tab_data.append(("Euphonic", ipw.HTML("euphonic_data"))) + euphonic_model = DielectricModel() + euphonic_widget = DielectricWidget( + 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] diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 014fad4..82d0aae 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -1,113 +1,227 @@ import numpy as np import traitlets as tl +import copy +import json +import base64 +import plotly.io as pio +from monty.json import jsanitize +from IPython.display import display -from aiidalab_qe_vibroscopy.utils.euphonic.intensity_maps import ( +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, ) -from aiidalab_qe.common.panel import ResultsModel +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(ResultsModel): +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 - - # check the SingleCrystalSettingsWidget and base + + # 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") - custom_kpath = tl.Unicode("") - + def fetch_data(self): - """Fetch the data from the database.""" + """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... - pass - + 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): - self.q_spacing = parameters.get("q_spacing", 0.01) - self.energy_broadening = parameters.get("energy_broadening", 0.05) - self.energy_bins = parameters.get("energy_bins", 200) - self.temperature = parameters.get("temperature", 0) - self.weighting = parameters.get("weighting", "coherent") - self.custom_kpath = parameters.get("custom_kpath", "") - + for k, v in parameters.items(): + setattr(self, k, v) + def _get_default(self, trait): - return self._defaults.get(trait, self.traits()[trait].default_value) - + return self.traits()[trait].default_value + def get_model_state(self): - return { - "q_spacing": self.q_spacing, - "energy_broadening": self.energy_broadening, - "energy_bins": self.energy_bins, - "temperature": self.temperature, - "weighting": self.weighting, - "custom_kpath": self.custom_kpath, - } - - def reset(self,): + return {trait: getattr(self, trait) for trait in self.traits()} + + def reset( + self, + ): with self.hold_trait_notifications(): - self.q_spacing = 0.01 - self.energy_broadening = 0.5 - self.energy_bins = 200 - self.temperature = 0 - self.weight_button = "coherent" - self.custom_kpath = "" - - - def _update_spectra(self,): - # can't do directly the following, as we want to have also detached app. - #if not (process_node := self.fetch_process_node()): - # return - - # AAA - # here I should generate the spectra with respect to the data I have, - # i.e. the FC and the parameters. - # I need to fetch the FC if are not there, but I suppose are already there. - # the spectrum is initialized in the full sc widget. - - spectra, parameters = produce_bands_weigthed_data( - params=self.get_model_state(), - fc=self.fc, - linear_path=self.q_path, - plot=False, # CHANGED - ) - - self.x, self.y = np.meshgrid(spectra[0].x_data.magnitude, spectra[0].y_data.magnitude) - - # This is used in order to have an overall intensity scale. - self.intensity_ref_0K = np.max(spectra[0].z_data.magnitude) # CHANGED + 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...) - ( - self.final_xspectra, - self.final_zspectra, - self.ticks_positions, - self.ticks_labels, - ) = generated_curated_data(spectra) - - + 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 = [] @@ -125,7 +239,10 @@ def curate_path_and_labels( scoords.append(l) coordinates.append(scoords) return coordinates, labels - + + def _clone(self): + return copy.deepcopy(self) + def download_data(self, _=None): """ Download both the ForceConstants and the spectra json files. @@ -180,4 +297,4 @@ def _download(payload, filename): document.body.removeChild(link); """.format(payload=payload, filename=filename) ) - display(javas) \ No newline at end of file + display(javas) 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..b11f245 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py @@ -0,0 +1,303 @@ +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, 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.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.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._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" + + +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/__init__.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py index 9b41707..e69de29 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py @@ -1,336 +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 - -from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import EuphonicBaseResultsModel - -###### 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', model=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.upload_widget = UploadPhonopyWidget() - self.upload_widget.reset_uploads.on_click(self._on_reset_uploads_button_clicked) - self._model.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._model.fc = fc - - self._model.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( # use the loading widget! - 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._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 _generate_force_constants( #needs to go in the model - self, - ): - if self.mode == "aiidalab-qe app plugin": - return self.fc # we should not check mode, but node, when we put this in the model. - - else: - 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, - ) - - return fc - - 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.fc = self._generate_force_constants() # should be in the model. - - # 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) - - 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 74% 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 86f26b9..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,42 +42,27 @@ class StructureFactorBasePlotWidget(ipw.VBox): """ THz_to_meV = 4.13566553853599 # conversion factor. + THz_to_cm1 = 33.3564095198155 # conversion factor. - def __init__(self, model, **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._model = model - - final_xspectra = self._model.spectra - - 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 @@ -112,51 +96,30 @@ def __init__(self, model, **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%", - ), - ) - - def _update_plot( # actually, should be an _update_plot... we don't modify data... - self, - ): - # this will be called in the _update_plot method of SingleCrystalPlotWidget and PowderPlotWidget + self.children = [ + self.fig, + ipw.HBox([ipw.HTML("Intensity window (%):"), self.slider_intensity]), + self.specification_intensity, + self.E_units_button, + ] + + 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. @@ -191,9 +154,15 @@ class StructureFactorSettingsBaseWidget(ipw.VBox): def __init__(self, model, **kwargs): super().__init__() - self._model = model - + self.rendered = False + + def render(self): + """Render the widget.""" + + if self.rendered: + return + self.q_spacing = ipw.FloatText( value=self._model.q_spacing, step=0.001, @@ -281,7 +250,7 @@ def __init__(self, model, **kwargs): button_style="primary", disabled=False, # Large files... layout=ipw.Layout(width="auto"), - ) + ) def _on_plot_button_changed(self, change): if change["new"] != change["old"]: @@ -289,12 +258,14 @@ def _on_plot_button_changed(self, change): def _on_weight_button_change(self, change): if change["new"] != change["old"]: - self.temperature.value = 0 + 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): # think if we want to do something more evident... + def _on_setting_changed( + self, change + ): # think if we want to do something more evident... self.plot_button.disabled = False - + def _reset_settings(self, _): - self._model.reset() \ No newline at end of file + 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 23e36ec..66095ca 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/intensity_maps.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py @@ -433,6 +433,7 @@ def produce_powder_data( params: Optional[List[str]] = parameters_powder, fc: ForceConstants = None, plot=False, + linear_path=None, ) -> None: blockPrint() @@ -760,14 +761,14 @@ def generate_force_constant_instance( return fc -def export_euphonic_data(node, fermi_energy=None): - if "phonon_bands" not in node.outputs: +def export_euphonic_data(output_vibronic, fermi_energy=None): + if "phonon_bands" not in output_vibronic: return None - output_set = node.outputs.phonon_bands + output_set = output_vibronic.phonon_bands - if any(not element for element in node.inputs.structure.pbc): - vibro_bands = node.inputs.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..d9c8019 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=[ @@ -200,26 +216,8 @@ 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.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 +227,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..9668473 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,48 @@ 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 - - super().__init__( - children=[ - self.title_intensity, - # self.map_widget, - self.settings_intensity, - ], - ) + self.children = [ + self.title_intensity, + # self.map_widget, + self.settings_intensity, + ] 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, - ) - - 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._model._update_qsection_spectra() - 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 +412,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 63% 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 206e8af..01c7906 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,18 +21,11 @@ class SingleCrystalPlotWidget(StructureFactorBasePlotWidget): - # I should introduce the model here. - def __init__(self, model, **kwargs): - - # Create and show figure; before, I was initializing this at the end. - super().__init__( - model, - **kwargs, - ) + def render(self): + if self.rendered: + return - # if we divide the max by this intensity of ref, does not change anything: the highest will always be 100% - - self.fig = go.FigureWidget() + super().render() heatmap_trace = go.Heatmap( z=self._model.final_zspectra.T, @@ -57,27 +43,39 @@ def __init__(self, model, **kwargs): # Add heatmap trace to figure self.fig.add_trace(heatmap_trace) + # Layout settings self.fig.update_layout( xaxis=dict( - tickmode="array", - tickvals=self._model.ticks_positions, - ticktext=self._model.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])]) - def _update_plot(self, spectra): # should be an update plot, the data should be re-generated in the model. - # apart the first two calls, I think the control of the view should here. - # we should do self._model._update_spectra and then self._update_plot here. - - self._model._update_spectra() - # Data to contour is the sum of two Gaussian functions. + self.fig["layout"]["yaxis"].update( + title="meV", + range=[min(self._model.y[:, 0]), max(self._model.y[:, 0])], + ) - # 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. + 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) + + # 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( @@ -88,7 +86,7 @@ def _update_plot(self, spectra): # should be an update plot, the data should be if self.E_units_button.value == "meV" else self._model.y[:, 0] ), - x=x, # self._model.x, + x=x, # self._model.x, colorbar=COLORBAR_DICT, colorscale=COLORSCALE, # imported from euphonic_base_widgets ) @@ -98,7 +96,9 @@ def _update_plot(self, spectra): # should be an update plot, the data should be # this is delays things self.fig.update_layout( xaxis=dict( - tickmode="array", tickvals=self._model.ticks_positions, ticktext=self._model.ticks_labels + tickmode="array", + tickvals=self._model.ticks_positions, + ticktext=self._model.ticks_labels, ) ) @@ -108,12 +108,12 @@ def _update_plot(self, spectra): # should be an update plot, the data should be class SingleCrystalSettingsWidget(StructureFactorSettingsBaseWidget): - def __init__(self, model, **kwargs): - # 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. + def render(self): + if self.rendered: + return + + super().render() - super().__init__(model=model) - self.custom_kpath_description = ipw.HTML( """
@@ -154,7 +154,6 @@ def __init__(self, model, **kwargs): self.download_button, ] ), - self.specification_intensity, self.q_spacing, self.energy_broadening, self.energy_bins, @@ -183,20 +182,26 @@ 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, model, **kwargs): - + def __init__(self, model): self._model = model - - # should be rendered, not done here! - # and moreover, should be done in the model. - self._model._update_spectra() + self.rendered = False + super().__init__() + + def render(self): + if self.rendered: + return self.title_intensity = ipw.HTML( "

Neutron dynamic structure factor - Single Crystal

" @@ -205,75 +210,38 @@ def __init__(self, model, **kwargs): # 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) - - - super().__init__( - children=[ - self.title_intensity, - self.map_widget, - self.settings_intensity, - ], - ) + self.children = [ + self.title_intensity, + self.map_widget, + self.settings_intensity, + ] 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_plot(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]) @@ -295,7 +263,6 @@ def download_data(self, _=None): b64_str = base64.b64encode(image_bytes).decode() self._download(payload=b64_str, filename=filename + ".png") - @staticmethod def _download(payload, filename): from IPython.display import Javascript