diff --git a/src/aiidalab_qe_vibroscopy/app/result/result.py b/src/aiidalab_qe_vibroscopy/app/result/result.py index c3cc01b..2ce571a 100644 --- a/src/aiidalab_qe_vibroscopy/app/result/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result/result.py @@ -13,6 +13,13 @@ 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, +) + class VibroResultsPanel(ResultsPanel[VibroResultsModel]): title = "Vibronic" @@ -58,9 +65,8 @@ def render(self): tab_data.append(("Raman/IR spectra", irraman_widget)) - needs_dielectri_tab = self._model.needs_dielectric_tab() - - if needs_dielectri_tab: + needs_dielectric_tab = self._model.needs_dielectric_tab() + if needs_dielectric_tab: dielectric_model = DielectricModel() dielectric_widget = DielectricWidget( model=dielectric_model, @@ -68,8 +74,14 @@ def render(self): ) tab_data.append(("Dielectric Properties", dielectric_widget)) - if self._model.needs_euphonic_tab(): - tab_data.append(("Euphonic", ipw.HTML("euphonic_data"))) + needs_euphonic_tab = self._model.needs_euphonic_tab() + if needs_euphonic_tab: + euphonic_model = EuphonicModel() + euphonic_widget = EuphonicWidget( + model=euphonic_model, + node=vibro_node, + ) + tab_data.append(("Neutron scattering", euphonic_widget)) # Assign children and titles dynamically self.tabs.children = [content for _, content in tab_data] diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py new file mode 100644 index 0000000..687b838 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -0,0 +1,252 @@ +import numpy as np +import traitlets as tl +import copy + +from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import ( + AttrDict, + produce_bands_weigthed_data, + produce_powder_data, + generated_curated_data, + par_dict, + par_dict_powder, + export_euphonic_data, + generate_force_constant_instance, +) + +from aiidalab_qe_vibroscopy.utils.euphonic.tab_widgets.euphonic_q_planes_widgets import ( + produce_Q_section_modes, + produce_Q_section_spectrum, +) + +from aiidalab_qe.common.mvc import Model + + +class EuphonicBaseResultsModel(Model): + """Model for the neutron scattering results panel.""" + + # Here we mode all the model and data-controller, i.e. all the data and their + # manipulation to produce new spectra. + # plot-controller, i.e. scales, colors and so on, should be attached to the widget, I think. + # the above last point should be discussed more. + + # For now, we do only the first of the following: + # 1. single crystal data: sc + # 2. powder average: pa + # 3. Q planes: qp + # TRY it on the detached app. + + # AAA TOBE defined with respect to the type + spectra = {} + path = [] + q_path = None + + # Settings for single crystal and powder average + q_spacing = tl.Float(0.01) + energy_broadening = tl.Float(0.05) + energy_bins = tl.Int(200) + temperature = tl.Float(0) + weighting = tl.Unicode("coherent") + + def fetch_data(self): + """Fetch the data from the database or from the uploaded files.""" + # 1. from aiida, so we have the node + if self.node: + ins_data = export_euphonic_data(self.node) + self.fc = ins_data["fc"] + self.q_path = ins_data["q_path"] + # 2. from uploaded files... + else: + self.fc = self.upload_widget._read_phonopy_files( + fname=self.fname, + phonopy_yaml_content=self._model.phonopy_yaml_content, + fc_hdf5_content=self._model.fc_hdf5_content, + ) + + def _inject_single_crystal_settings( + self, + ): + self.parameters = copy.deepcopy( + par_dict + ) # need to be different if powder or q section. + self._callback_spectra_generation = produce_bands_weigthed_data + # Dynamically add a trait for single crystal settings + self.add_traits(custom_kpath=tl.Unicode("")) + + def _inject_powder_settings( + self, + ): + self.parameters = copy.deepcopy(par_dict_powder) + self._callback_spectra_generation = produce_powder_data + # Dynamically add a trait for powder settings + self.add_traits(q_min=tl.Float(0.0)) + self.add_traits(q_max=tl.Float(1)) + self.add_traits(npts=tl.Int(100)) + + def _inject_qsection_settings( + self, + ): + # self._callback_spectra_generation = produce_Q_section_modes + # Dynamically add a trait for q section settings + self.add_traits(center_e=tl.Float(0.0)) + self.add_traits(Q0_vec=tl.List(trait=tl.Float(), default_value=[0.0, 0.0, 0.0])) + self.add_traits( + h_vec=tl.List(trait=tl.Float(), default_value=[1, 1, 1, 100, 1]) + ) + self.add_traits( + k_vec=tl.List(trait=tl.Float(), default_value=[1, 1, 1, 100, 1]) + ) + + def set_model_state(self, parameters: dict): + for k, v in parameters.items(): + setattr(self, k, v) + + def _get_default(self, trait): + if trait in ["h_vec", "k_vec"]: + return [1, 1, 1, 100, 1] + elif trait == "Q0_vec": + return [0.0, 0.0, 0.0] + return self.traits()[trait].default_value + + def get_model_state(self): + return {trait: getattr(self, trait) for trait in self.traits()} + + def reset( + self, + ): + with self.hold_trait_notifications(): + for trait in self.traits(): + setattr(self, trait, self._get_default(trait)) + + def _update_spectra( + self, + ): + # This is used to update the spectra when the parameters are changed + # and the + if not hasattr(self, "parameters"): + self._inject_single_crystal_settings() + + self.parameters.update(self.get_model_state()) + parameters_ = AttrDict(self.parameters) + + # custom linear path + custom_kpath = self.custom_kpath if hasattr(self, "custom_kpath") else "" + if len(custom_kpath) > 1: + coordinates, labels = self.curate_path_and_labels() + qpath = { + "coordinates": coordinates, + "labels": labels, # ["$\Gamma$","X","X","(1,1,1)"], + "delta_q": parameters_["q_spacing"], + } + else: + qpath = copy.deepcopy(self.q_path) + if qpath: + qpath["delta_q"] = parameters_["q_spacing"] + + spectra, parameters = self._callback_spectra_generation( + params=parameters_, + fc=self.fc, + linear_path=qpath, + plot=False, + ) + + # curated spectra (labels and so on...) + if hasattr(self, "custom_kpath"): # single crystal case + self.x, self.y = np.meshgrid( + spectra[0].x_data.magnitude, spectra[0].y_data.magnitude + ) + ( + self.final_xspectra, + self.final_zspectra, + self.ticks_positions, + self.ticks_labels, + ) = generated_curated_data(spectra) + else: + # Spectrum2D as output of the powder data + self.x, self.y = np.meshgrid( + spectra.x_data.magnitude, spectra.y_data.magnitude + ) + + # we don't need to curate the powder data, + # we can directly use them: + self.final_xspectra = spectra.x_data.magnitude + self.final_zspectra = spectra.z_data.magnitude + + def _update_qsection_spectra( + self, + ): + parameters_ = AttrDict( + { + "h": np.array([i for i in self.h_vec[:-2]]), + "k": np.array([i for i in self.k_vec[:-2]]), + "n_h": int(self.h_vec[-2]), + "n_k": int(self.k_vec[-2]), + "h_extension": self.h_vec[-1], + "k_extension": self.k_vec[-1], + "Q0": np.array([i for i in self.Q0_vec[:]]), + "ecenter": self.center_e, + "deltaE": self.energy_broadening, + "bins": self.energy_bins, + "spectrum_type": self.weighting, + "temperature": self.temperature, + } + ) + + modes, q_array, h_array, k_array, labels, dw = produce_Q_section_modes( + self.fc, + h=parameters_.h, + k=parameters_.k, + Q0=parameters_.Q0, + n_h=parameters_.n_h, + n_k=parameters_.n_k, + h_extension=parameters_.h_extension, + k_extension=parameters_.k_extension, + temperature=parameters_.temperature, + ) + + self.av_spec, self.q_array, self.h_array, self.k_array, self.labels = ( + produce_Q_section_spectrum( + modes, + q_array, + h_array, + k_array, + ecenter=parameters_.ecenter, + deltaE=parameters_.deltaE, + bins=parameters_.bins, + spectrum_type=parameters_.spectrum_type, + dw=dw, + labels=labels, + ) + ) + + def curate_path_and_labels( + self, + ): + # This is used to curate the path and labels of the spectra if custom kpath is provided. + # I do not like this implementation (MB) + coordinates = [] + labels = [] + path = self.custom_kpath + linear_paths = path.split("|") + for i in linear_paths: + scoords = [] + s = i.split( + " - " + ) # not i.split("-"), otherwise also the minus of the negative numbers are used for the splitting. + for k in s: + labels.append(k.strip()) + # AAA missing support for fractions. + l = tuple(map(float, [kk for kk in k.strip().split(" ")])) # noqa: E741 + scoords.append(l) + coordinates.append(scoords) + return coordinates, labels + + def _clone(self): + return copy.deepcopy(self) + + def produce_phonopy_files(self): + # This is used to produce the phonopy files from + # PhonopyCalculation data. The files are phonopy.yaml and force_constants.hdf5 + phonopy_yaml, fc_hdf5 = generate_force_constant_instance( + self.node.phonon_bands.creator, mode="download" + ) + return phonopy_yaml, fc_hdf5 diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py new file mode 100644 index 0000000..00024e8 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py @@ -0,0 +1,310 @@ +import pathlib +import tempfile + + +from IPython.display import display + +import ipywidgets as ipw + +# from ..euphonic.bands_pdos import * +from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import ( + generate_force_constant_instance, + export_euphonic_data, # noqa: F401 +) +from aiidalab_qe_vibroscopy.utils.euphonic.tab_widgets.euphonic_single_crystal_widgets import ( + SingleCrystalFullWidget, +) +from aiidalab_qe_vibroscopy.utils.euphonic.tab_widgets.euphonic_powder_widgets import ( + PowderFullWidget, +) +from aiidalab_qe_vibroscopy.utils.euphonic.tab_widgets.euphonic_q_planes_widgets import ( + QSectionFullWidget, +) + + +from aiidalab_qe.common.widgets import LoadingWidget +###### START for detached app: + + +# Upload buttons +class UploadPhonopyYamlWidget(ipw.FileUpload): + def __init__(self, **kwargs): + super().__init__( + description="upload phonopy YAML file", + multiple=False, + layout={"width": "initial"}, + ) + + +class UploadForceConstantsHdf5Widget(ipw.FileUpload): + def __init__(self, **kwargs): + super().__init__( + description="upload force constants HDF5 file", + multiple=False, + layout={"width": "initial"}, + ) + + +class UploadPhonopyWidget(ipw.HBox): + def __init__(self, **kwargs): + self.upload_phonopy_yaml = UploadPhonopyYamlWidget(**kwargs) + self.upload_phonopy_hdf5 = UploadForceConstantsHdf5Widget(**kwargs) + + self.reset_uploads = ipw.Button( + description="Discard uploaded files", + icon="pencil", + button_style="warning", + disabled=False, + layout=ipw.Layout(width="auto"), + ) + + super().__init__( + children=[ + self.upload_phonopy_yaml, + self.upload_phonopy_hdf5, + self.reset_uploads, + ], + **kwargs, + ) + + def _read_phonopy_files(self, fname, phonopy_yaml_content, fc_hdf5_content=None): + suffix = "".join(pathlib.Path(fname).suffixes) + + with tempfile.NamedTemporaryFile(suffix=suffix) as temp_yaml: + temp_yaml.write(phonopy_yaml_content) + temp_yaml.flush() + + if fc_hdf5_content: + with tempfile.NamedTemporaryFile(suffix=".hdf5") as temp_file: + temp_file.write(fc_hdf5_content) + temp_file.flush() + temp_hdf5_name = temp_file.name + + try: + fc = generate_force_constant_instance( + path=pathlib.Path(fname), + summary_name=temp_yaml.name, + fc_name=temp_hdf5_name, + ) + except ValueError: + return None + + return fc + else: + temp_hdf5_name = None + + try: + fc = generate_force_constant_instance( + path=pathlib.Path(fname), + summary_name=temp_yaml.name, + # fc_name=temp_hdf5_name, + ) + except ValueError: + return None + + return fc + + +#### END for detached app + + +##### START OVERALL WIDGET TO DISPLAY EVERYTHING: + + +class EuphonicSuperWidget(ipw.VBox): + """ + Widget that will include everything, + from the upload widget to the tabs with single crystal and powder predictions. + In between, we trigger the initialization of plots via a button. + """ + + def __init__( + self, mode="aiidalab-qe app plugin", model=None, node=None, fc=None, q_path=None + ): + """ + Initialize the Euphonic utility class. + Parameters: + ----------- + mode : str, optional + The mode of operation, default is "aiidalab-qe app plugin". + fc : optional + Force constants, default is None. + q_path : optional + Q-path for phonon calculations, default is None. If Low-D system, this can be provided. + It is the same path obtained for the PhonopyCalculation of the phonopy_bands. + Attributes: + ----------- + mode : str + The mode of operation. + upload_widget : UploadPhonopyWidget + Widget for uploading phonopy files. + fc_hdf5_content : None + Content of the force constants HDF5 file. + tab_widget : ipw.Tab + Tab widget for different views. + plot_button : ipw.Button + Button to initialize INS data. + fc : optional + Force constants if provided. + """ + + self.mode = mode + self._model = model # this is the single crystal model. + self._model.node = node + self._model.fc_hdf5_content = None + + self.rendered = False + + super().__init__() + + def render(self): + if self.rendered: + return + + self.upload_widget = UploadPhonopyWidget() + self.upload_widget.reset_uploads.on_click(self._on_reset_uploads_button_clicked) + + self.tab_widget = ipw.Tab() + self.tab_widget.layout.display = "none" + self.tab_widget.set_title(0, "Single crystal") + self.tab_widget.set_title(1, "Powder sample") + self.tab_widget.set_title(2, "Q-plane view") + self.tab_widget.children = () + + self.plot_button = ipw.Button( + description="Initialise INS data", + icon="pencil", + button_style="primary", + disabled=True, + layout=ipw.Layout(width="auto"), + ) + self.plot_button.on_click(self._on_first_plot_button_clicked) + + self.loading_widget = LoadingWidget("Loading INS data") + self.loading_widget.layout.display = "none" + + if self.mode == "aiidalab-qe app plugin": + self.upload_widget.layout.display = "none" + self.plot_button.disabled = False + else: + self.upload_widget.children[0].observe(self._on_upload_yaml, "_counter") + self.upload_widget.children[1].observe(self._on_upload_hdf5, "_counter") + + self.download_widget = DownloadYamlHdf5Widget(model=self._model) + self.download_widget.layout.display = "none" + + self.children = [ + self.upload_widget, + self.plot_button, + self.loading_widget, + self.tab_widget, + self.download_widget, + ] + + def _on_reset_uploads_button_clicked(self, change): + self.upload_widget.upload_phonopy_yaml.value.clear() + self.upload_widget.upload_phonopy_yaml._counter = 0 + self.upload_widget.upload_phonopy_hdf5.value.clear() + self.upload_widget.upload_phonopy_hdf5._counter = 0 + + self.plot_button.layout.display = "block" + self.plot_button.disabled = True + + self.tab_widget.children = () + + self.tab_widget.layout.display = "none" + + def _on_upload_yaml(self, change): + if change["new"] != change["old"]: + for fname in self.upload_widget.children[ + 0 + ].value.keys(): # always one key because I allow only one file at the time. + self.fname = fname + self._model.phonopy_yaml_content = self.upload_widget.children[0].value[ + fname + ]["content"] + + if self.plot_button.disabled: + self.plot_button.disabled = False + + def _on_upload_hdf5(self, change): + if change["new"] != change["old"]: + for fname in self.upload_widget.children[1].value.keys(): + self._model.fc_hdf5_content = self.upload_widget.children[1].value[ + fname + ]["content"] + + def _on_first_plot_button_clicked(self, change=None): # basically the render. + # It creates the widgets + self.plot_button.layout.display = "none" + self.loading_widget.layout.display = "block" + + self._model.fetch_data() # should be in the model. + powder_model = self._model._clone() + qsection_model = self._model._clone() + + # I first initialise this widget, to then have the 0K ref for the other two. + # the model is passed to the widget. For the other two, I need to generate the model. + singlecrystalwidget = SingleCrystalFullWidget(model=self._model) + + # I need to generate the models for the other two widgets. + self._model._inject_single_crystal_settings() + powder_model._inject_powder_settings() + qsection_model._inject_qsection_settings() + + self.tab_widget.children = ( + singlecrystalwidget, + PowderFullWidget(model=powder_model), + QSectionFullWidget(model=qsection_model), + ) + + for widget in self.tab_widget.children: + widget.render() # this is the render method of the widget. + + self.loading_widget.layout.display = "none" + self.tab_widget.layout.display = "block" + self.download_widget.layout.display = "block" + + +class DownloadYamlHdf5Widget(ipw.HBox): + def __init__(self, model): + self._model = model + + self.download_button = ipw.Button( + description="Download phonopy data", + icon="pencil", + button_style="primary", + disabled=False, + layout=ipw.Layout(width="auto"), + ) + self.download_button.on_click(self._download_data) + + super().__init__( + children=[ + self.download_button, + ], + ) + + def _download_data(self, _=None): + """ + Download both the phonopy.yaml and fc.hdf5 files. + """ + phonopy_yaml, fc_hdf5 = self._model.produce_phonopy_files() + self._download(payload=phonopy_yaml, filename="phonopy" + ".yaml") + self._download(payload=fc_hdf5, filename="fc" + ".hdf5") + + @staticmethod + def _download(payload, filename): + from IPython.display import Javascript + + javas = Javascript( + """ + var link = document.createElement('a'); + link.href = 'data:text/json;charset=utf-8;base64,{payload}' + link.download = "{filename}" + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + """.format(payload=payload, filename=filename) + ) + display(javas) diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb b/src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb index 98a0cfc..e66b899 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb @@ -8,7 +8,12 @@ "outputs": [ { "data": { - "application/javascript": "IPython.OutputArea.prototype._should_scroll = function(lines) {\n return false;\n}\ndocument.title='aiidalab-qe-vibroscopy detached app'\n", + "application/javascript": [ + "IPython.OutputArea.prototype._should_scroll = function(lines) {\n", + " return false;\n", + "}\n", + "document.title='aiidalab-qe-vibroscopy detached app'\n" + ], "text/plain": [ "" ] @@ -30,7 +35,56 @@ "execution_count": 2, "id": "a8549863", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a2abe40204ac4a478c99cb6f8ba683b4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "

Warning:

\n", + "

The default profile 'default' was loaded automatically. This behavior will be removed in the v3.0.0. Please load the profile manually before loading modules from aiidalab-widgets-base by adding the following code at the beginning cell of the notebook:

\n", + "
\n",
+       "from aiida import load_profile\n",
+       "load_profile();
\n", + "
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + " var style = document.createElement('style');\n", + " style.type = 'text/css';\n", + " style.innerHTML = ``;\n", + " document.head.appendChild(style);\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Fix pybel import path\n", "try:\n", @@ -52,10 +106,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "586bd730", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "aec117638bca4fb998fcec2ce07f9d0d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "env = Environment()\n", "\n", @@ -66,7 +135,9 @@ " '

Copyright (c) 2024 Miki Bonacci (PSI), miki.bonacci@psi.ch;  Version: 0.1.1

'\n", ")\n", "\n", - "widget = EuphonicSuperWidget(mode=\"detached\")\n", + "from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import EuphonicBaseResultsModel\n", + "\n", + "widget = EuphonicSuperWidget(mode=\"detached\", model=EuphonicBaseResultsModel())\n", "\n", "output = ipw.Output()\n", "\n", @@ -75,6 +146,14 @@ "\n", "display(output)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22387f54", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py index 0d60410..e69de29 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/__init__.py @@ -1,334 +0,0 @@ -import pathlib -import tempfile - - -from IPython.display import display - -import ipywidgets as ipw - -# from ..euphonic.bands_pdos import * -from .intensity_maps import ( - generate_force_constant_instance, - export_euphonic_data, # noqa: F401 -) -from .euphonic_single_crystal_widgets import SingleCrystalFullWidget -from .euphonic_powder_widgets import PowderFullWidget -from .euphonic_q_planes_widgets import QSectionFullWidget - - -###### START for detached app: - -# spinner for waiting time (supercell estimations) -spinner_html = """ - -
-
-
-""" - - -# Upload buttons -class UploadPhonopyYamlWidget(ipw.FileUpload): - def __init__(self, **kwargs): - super().__init__( - description="upload phonopy YAML file", - multiple=False, - layout={"width": "initial"}, - ) - - -class UploadForceConstantsHdf5Widget(ipw.FileUpload): - def __init__(self, **kwargs): - super().__init__( - description="upload force constants HDF5 file", - multiple=False, - layout={"width": "initial"}, - ) - - -class UploadPhonopyWidget(ipw.HBox): - def __init__(self, **kwargs): - self.upload_phonopy_yaml = UploadPhonopyYamlWidget(**kwargs) - self.upload_phonopy_hdf5 = UploadForceConstantsHdf5Widget(**kwargs) - - self.reset_uploads = ipw.Button( - description="Discard uploaded files", - icon="pencil", - button_style="warning", - disabled=False, - layout=ipw.Layout(width="auto"), - ) - - super().__init__( - children=[ - self.upload_phonopy_yaml, - self.upload_phonopy_hdf5, - self.reset_uploads, - ], - **kwargs, - ) - - def _read_phonopy_files(self, fname, phonopy_yaml_content, fc_hdf5_content=None): - suffix = "".join(pathlib.Path(fname).suffixes) - - with tempfile.NamedTemporaryFile(suffix=suffix) as temp_yaml: - temp_yaml.write(phonopy_yaml_content) - temp_yaml.flush() - - if fc_hdf5_content: - with tempfile.NamedTemporaryFile(suffix=".hdf5") as temp_file: - temp_file.write(fc_hdf5_content) - temp_file.flush() - temp_hdf5_name = temp_file.name - - try: - fc = generate_force_constant_instance( - path=pathlib.Path(fname), - summary_name=temp_yaml.name, - fc_name=temp_hdf5_name, - ) - except ValueError: - return None - - return fc - else: - temp_hdf5_name = None - - try: - fc = generate_force_constant_instance( - path=pathlib.Path(fname), - summary_name=temp_yaml.name, - # fc_name=temp_hdf5_name, - ) - except ValueError: - return None - - return fc - - -#### END for detached app - - -##### START OVERALL WIDGET TO DISPLAY EVERYTHING: - - -class EuphonicSuperWidget(ipw.VBox): - """ - Widget that will include everything, - from the upload widget to the tabs with single crystal and powder predictions. - In between, we trigger the initialization of plots via a button. - """ - - def __init__(self, mode="aiidalab-qe app plugin", fc=None, q_path=None): - """ - Initialize the Euphonic utility class. - Parameters: - ----------- - mode : str, optional - The mode of operation, default is "aiidalab-qe app plugin". - fc : optional - Force constants, default is None. - q_path : optional - Q-path for phonon calculations, default is None. If Low-D system, this can be provided. - It is the same path obtained for the PhonopyCalculation of the phonopy_bands. - Attributes: - ----------- - mode : str - The mode of operation. - upload_widget : UploadPhonopyWidget - Widget for uploading phonopy files. - fc_hdf5_content : None - Content of the force constants HDF5 file. - tab_widget : ipw.Tab - Tab widget for different views. - plot_button : ipw.Button - Button to initialize INS data. - fc : optional - Force constants if provided. - """ - - self.mode = mode - - self.upload_widget = UploadPhonopyWidget() - self.upload_widget.reset_uploads.on_click(self._on_reset_uploads_button_clicked) - self.fc_hdf5_content = None - - self.tab_widget = ipw.Tab() - self.tab_widget.layout.display = "none" - self.tab_widget.set_title(0, "Single crystal") - self.tab_widget.set_title(1, "Powder sample") - self.tab_widget.set_title(2, "Q-plane view") - self.tab_widget.children = () - - if fc: - self.fc = fc - - self.q_path = q_path - - self.plot_button = ipw.Button( - description="Initialise INS data", - icon="pencil", - button_style="primary", - disabled=True, - layout=ipw.Layout(width="auto"), - ) - self.plot_button.on_click(self._on_first_plot_button_clicked) - - self.loading_widget = ipw.HTML( - value=spinner_html, - ) - self.loading_widget.layout.display = "none" - - if self.mode == "aiidalab-qe app plugin": - self.upload_widget.layout.display = "none" - self.plot_button.disabled = False - else: - self.upload_widget.children[0].observe(self._on_upload_yaml, "_counter") - self.upload_widget.children[1].observe(self._on_upload_hdf5, "_counter") - - super().__init__( - children=[ - self.upload_widget, - self.plot_button, - self.loading_widget, - self.tab_widget, - ], - ) - - def _on_reset_uploads_button_clicked(self, change): - self.upload_widget.upload_phonopy_yaml.value.clear() - self.upload_widget.upload_phonopy_yaml._counter = 0 - self.upload_widget.upload_phonopy_hdf5.value.clear() - self.upload_widget.upload_phonopy_hdf5._counter = 0 - - self.plot_button.layout.display = "block" - self.plot_button.disabled = True - - self.tab_widget.children = () - - self.tab_widget.layout.display = "none" - - def _on_upload_yaml(self, change): - if change["new"] != change["old"]: - for fname in self.upload_widget.children[ - 0 - ].value.keys(): # always one key because I allow only one file at the time. - self.fname = fname - self.phonopy_yaml_content = self.upload_widget.children[0].value[fname][ - "content" - ] - - if self.plot_button.disabled: - self.plot_button.disabled = False - - def _on_upload_hdf5(self, change): - if change["new"] != change["old"]: - for fname in self.upload_widget.children[1].value.keys(): - self.fc_hdf5_content = self.upload_widget.children[1].value[fname][ - "content" - ] - - def _generate_force_constants( - self, - ): - if self.mode == "aiidalab-qe app plugin": - return self.fc - - else: - fc = self.upload_widget._read_phonopy_files( - fname=self.fname, - phonopy_yaml_content=self.phonopy_yaml_content, - fc_hdf5_content=self.fc_hdf5_content, - ) - - return fc - - def _on_first_plot_button_clicked(self, change=None): - # It creates the widgets - self.plot_button.layout.display = "none" - - self.loading_widget.layout.display = "block" - - self.fc = self._generate_force_constants() - - # I first initialise this widget, to then have the 0K ref for the other two. - singlecrystalwidget = SingleCrystalFullWidget(self.fc, self.q_path) - - self.tab_widget.children = ( - singlecrystalwidget, - PowderFullWidget( - self.fc, intensity_ref_0K=singlecrystalwidget.intensity_ref_0K - ), - QSectionFullWidget( - self.fc, intensity_ref_0K=singlecrystalwidget.intensity_ref_0K - ), - ) - - self.loading_widget.layout.display = "none" - - self.tab_widget.layout.display = "block" - - -class DownloadYamlHdf5Widget(ipw.HBox): - def __init__(self, phonopy_node, **kwargs): - self.download_button = ipw.Button( - description="Download phonopy data", - icon="pencil", - button_style="primary", - disabled=False, - layout=ipw.Layout(width="auto"), - ) - self.download_button.on_click(self.download_data) - self.node = phonopy_node - - super().__init__( - children=[ - self.download_button, - ], - ) - - def download_data(self, _=None): - """ - Download both the phonopy.yaml and fc.hdf5 files. - """ - phonopy_yaml, fc_hdf5 = generate_force_constant_instance( - self.node, mode="download" - ) - self._download(payload=phonopy_yaml, filename="phonopy" + ".yaml") - self._download(payload=fc_hdf5, filename="fc" + ".hdf5") - - @staticmethod - def _download(payload, filename): - from IPython.display import Javascript - - javas = Javascript( - """ - var link = document.createElement('a'); - link.href = 'data:text/json;charset=utf-8;base64,{payload}' - link.download = "{filename}" - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - """.format(payload=payload, filename=filename) - ) - display(javas) diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_base_widgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py similarity index 50% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_base_widgets.py rename to src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py index 1958068..b0daf63 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_base_widgets.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py @@ -1,10 +1,9 @@ -from IPython.display import display - import numpy as np import ipywidgets as ipw +import plotly.graph_objects as go # from ..euphonic.bands_pdos import * -from .intensity_maps import * # noqa: F403 +from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import * # noqa: F403 # sys and os used to prevent euphonic to print in the stdout. @@ -43,38 +42,27 @@ class StructureFactorBasePlotWidget(ipw.VBox): """ THz_to_meV = 4.13566553853599 # conversion factor. + THz_to_cm1 = 33.3564095198155 # conversion factor. - def __init__(self, final_xspectra, **kwargs): - """ + def __init__(self, model): + super().__init__() + self._model = model + self.rendered = False - Args: - final_xspectra (_type_): + def render(self): + """Render the widget. + This is the generic render method which can be overwritten by the subwidgets. + However, it is important to call this method at the start of the subwidgets.render() in order to have the go.FigureWidget. """ - self.message_fig = ipw.HTML("") - self.message_fig.layout.display = "none" - - if self.fig.layout.images: - for image in self.fig.layout.images: - image["scl"] = 2 # Set the scale for each image - - self.fig["layout"]["xaxis"].update( - range=[min(final_xspectra), max(final_xspectra)] - ) - - self.fig["layout"]["yaxis"].update(title="meV") + if self.rendered: + return - self.fig.update_layout( - height=500, - width=700, - margin=dict(l=15, r=15, t=15, b=15), - ) - # Update x-axis and y-axis to enable autoscaling - self.fig.update_xaxes(autorange=True) - self.fig.update_yaxes(autorange=True) + if not hasattr(self._model, "fc"): + self._model.fetch_data() + self._model._update_spectra() - # Update the layout to enable autoscaling - self.fig.update_layout(autosize=True) + self.fig = go.FigureWidget() self.slider_intensity = ipw.FloatRangeSlider( value=[1, 100], # Default selected interval @@ -108,52 +96,30 @@ def __init__(self, final_xspectra, **kwargs): ), ) self.E_units_button.observe(self._update_energy_units, "value") + # MAYBE WE LINK ALSO THIS TO THE MODEL? so we can download the data with the preferred units. # Create and show figure - super().__init__( - children=[ - self.message_fig, - self.fig, - ipw.HBox([ipw.HTML("Intensity window (%):"), self.slider_intensity]), - self.specification_intensity, - self.E_units_button, - ], - layout=ipw.Layout( - width="100%", - ), - ) + self.children = [ + self.fig, + ipw.HBox([ipw.HTML("Intensity window (%):"), self.slider_intensity]), + self.specification_intensity, + self.E_units_button, + ] - def _update_spectra( - self, - final_zspectra, - ): - # this will be called in the _update_spectra method of SingleCrystalPlotWidget and PowderPlotWidget + def _update_plot(self): + """This is the generic update_plot method which can be overwritten by the subwidgets. + However, it is important to call this method at the end of the subwidgets._update_plot() in order to update the intensity window. + """ - # Update the layout to enable autoscaling self.fig.update_layout(autosize=True) - # We should do a check, if we have few points (<200?) provide like a warning.. - # Also decise less than what, 30%, 50%...? - - """ - visible_points = len( - np.where(self.fig.data[0].z > 0.5)[0] - ) - if visible_points < 1000: - message = f"Only {visible_points}/{len(final_zspectra.T)} points have intensity higher than 50%" - self.message_fig.value = message - self.message_fig.layout.display = "block" - else: - self.message_fig.layout.display = "none" - """ - # I have also to update the energy window. or better, to set the intensity to respect the current intensity window selected: self.fig.data[0].zmax = ( self.slider_intensity.value[1] * np.max(self.fig.data[0].z) / 100 - ) # above this, it is all yellow, i.e. max intensity. + ) # above this, it is all yellow, i.e. this is the max detachable intensity. self.fig.data[0].zmin = ( self.slider_intensity.value[0] * np.max(self.fig.data[0].z) / 100 - ) # above this, it is all yellow, i.e. max intensity. + ) # below this, it is all dark blue, i.e. this is the min detachable intensity. def _update_intensity_filter(self, change): # the value of the intensity slider is in fractions of the max. @@ -186,50 +152,78 @@ class StructureFactorSettingsBaseWidget(ipw.VBox): both single crystal or powder. """ - def __init__(self, **kwargs): + def __init__(self, model, **kwargs): super().__init__() + self._model = model + self.rendered = False - self.float_q_spacing = ipw.FloatText( - value=0.01, + def render(self): + """Render the widget.""" + + if self.rendered: + return + + self.q_spacing = ipw.FloatText( + value=self._model.q_spacing, step=0.001, description="q step (1/A)", tooltip="q spacing in 1/A", ) - self.float_q_spacing.observe(self._on_setting_changed, names="value") + ipw.link( + (self._model, "q_spacing"), + (self.q_spacing, "value"), + ) + self.q_spacing.observe(self._on_setting_changed, names="value") - self.float_energy_broadening = ipw.FloatText( - value=0.5, + self.energy_broadening = ipw.FloatText( + value=self._model.energy_broadening, step=0.01, description="ΔE (meV)", tooltip="Energy broadening in meV", ) - self.float_energy_broadening.observe(self._on_setting_changed, names="value") + ipw.link( + (self._model, "energy_broadening"), + (self.energy_broadening, "value"), + ) + self.energy_broadening.observe(self._on_setting_changed, names="value") - self.int_energy_bins = ipw.IntText( - value=200, + self.energy_bins = ipw.IntText( + value=self._model.energy_bins, description="#E bins", tooltip="Number of energy bins", ) - self.int_energy_bins.observe(self._on_setting_changed, names="value") + ipw.link( + (self._model, "energy_bins"), + (self.energy_bins, "value"), + ) + self.energy_bins.observe(self._on_setting_changed, names="value") - self.float_T = ipw.FloatText( - value=0, + self.temperature = ipw.FloatText( + value=self._model.temperature, step=0.01, description="T (K)", disabled=False, ) - self.float_T.observe(self._on_setting_changed, names="value") + ipw.link( + (self._model, "temperature"), + (self.temperature, "value"), + ) + self.temperature.observe(self._on_setting_changed, names="value") self.weight_button = ipw.ToggleButtons( options=[ ("Coherent", "coherent"), ("DOS", "dos"), ], - value="coherent", + value=self._model.weighting, description="weight:", disabled=False, style={"description_width": "initial"}, ) + ipw.link( + (self._model, "weighting"), + (self.weight_button, "value"), + ) self.weight_button.observe(self._on_weight_button_change, names="value") self.plot_button = ipw.Button( @@ -248,6 +242,7 @@ def __init__(self, **kwargs): disabled=False, layout=ipw.Layout(width="auto"), ) + self.reset_button.on_click(self._reset_settings) self.download_button = ipw.Button( description="Download Data and Plot", @@ -257,97 +252,20 @@ def __init__(self, **kwargs): layout=ipw.Layout(width="auto"), ) - self.reset_button.on_click(self._reset_settings) - - def _reset_settings(self, _): - self.float_q_spacing.value = 0.01 - self.float_energy_broadening.value = 0.5 - self.int_energy_bins.value = 200 - self.float_T.value = 0 - self.weight_button.value = "coherent" - def _on_plot_button_changed(self, change): if change["new"] != change["old"]: self.download_button.disabled = not change["new"] def _on_weight_button_change(self, change): if change["new"] != change["old"]: - self.float_T.value = 0 - self.float_T.disabled = True if change["new"] == "dos" else False + self._model.temperature = 0 + self.temperature.disabled = True if change["new"] == "dos" else False self.plot_button.disabled = False - def _on_setting_changed(self, change): + def _on_setting_changed( + self, change + ): # think if we want to do something more evident... self.plot_button.disabled = False - -class SingleCrystalSettingsWidget(StructureFactorSettingsBaseWidget): - def __init__(self, **kwargs): - self.custom_kpath_description = ipw.HTML( - """ -
- Custom q-points path for the structure factor:
- we can provide it via a specific format:
- (1) each linear path should be divided by '|';
- (2) each path is composed of 'qxi qyi qzi - qxf qyf qzf' where qxi and qxf are, respectively, - the start and end x-coordinate of the q direction, in reciprocal lattice units (rlu).
- An example path is: '0 0 0 - 1 1 1 | 1 1 1 - 0.5 0.5 0.5'.
- For now, we do not support fractions (i.e. we accept 0.5 but not 1/2). -
- """ - ) - - self.custom_kpath_text = ipw.Text( - value="", - description="Custom path (rlu):", - style={"description_width": "initial"}, - ) - custom_style = '' - display(ipw.HTML(custom_style)) - self.custom_kpath_text.add_class("custom-font") - - self.custom_kpath_text.observe(self._on_setting_changed, names="value") - - # Please note: if you change the order of the widgets below, it will - # affect the usage of the children[0] below in the full widget. - - super().__init__() - - self.children = [ - ipw.HBox( - [ - ipw.VBox( - [ - ipw.HBox( - [ - self.reset_button, - self.plot_button, - self.download_button, - ] - ), - self.specification_intensity, - self.float_q_spacing, - self.float_energy_broadening, - self.int_energy_bins, - self.float_T, - self.weight_button, - ], - layout=ipw.Layout( - width="50%", - ), - ), - ipw.VBox( - [ - self.custom_kpath_description, - self.custom_kpath_text, - ], - layout=ipw.Layout( - width="80%", - ), - ), - ], # end of HBox children - ), - ] - def _reset_settings(self, _): - self.custom_kpath_text.value = "" - super()._reset_settings(_) + self._model.reset() diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/bands_pdos.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/bands_pdos.py similarity index 100% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/bands_pdos.py rename to src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/bands_pdos.py diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/intensity_maps.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py similarity index 98% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/intensity_maps.py rename to src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py index 1e59580..66095ca 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/intensity_maps.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py @@ -231,7 +231,9 @@ def produce_bands_weigthed_data( if not params: args = AttrDict(copy.deepcopy(parameters)) else: - args = AttrDict(copy.deepcopy(params)) + args = copy.deepcopy(parameters) + args.update(params) + args = AttrDict(args) # redundancy with args... calc_modes_kwargs = _calc_modes_kwargs(args) @@ -431,6 +433,7 @@ def produce_powder_data( params: Optional[List[str]] = parameters_powder, fc: ForceConstants = None, plot=False, + linear_path=None, ) -> None: blockPrint() @@ -758,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 51% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/euphonic_single_crystal_widgets.py rename to src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_single_crystal_widgets.py index f03df86..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,23 +21,15 @@ class SingleCrystalPlotWidget(StructureFactorBasePlotWidget): - def __init__(self, spectra, intensity_ref_0K=1, **kwargs): - ( - final_xspectra, - final_zspectra, - ticks_positions, - ticks_labels, - ) = generated_curated_data(spectra) - # Data to contour is the sum of two Gaussian functions. - x, y = np.meshgrid(spectra[0].x_data.magnitude, spectra[0].y_data.magnitude) - - self.intensity_ref_0K = intensity_ref_0K + def render(self): + if self.rendered: + return - self.fig = go.FigureWidget() + super().render() heatmap_trace = go.Heatmap( - z=final_zspectra.T, - y=y[:, 0] * self.THz_to_meV, + z=self._model.final_zspectra.T, + y=self._model.y[:, 0] * self.THz_to_meV, x=None, colorbar=COLORBAR_DICT, colorscale=COLORSCALE, # imported from euphonic_base_widgets @@ -58,45 +43,50 @@ def __init__(self, spectra, intensity_ref_0K=1, **kwargs): # Add heatmap trace to figure self.fig.add_trace(heatmap_trace) + # Layout settings self.fig.update_layout( xaxis=dict( - tickmode="array", tickvals=ticks_positions, ticktext=ticks_labels + tickmode="array", + tickvals=self._model.ticks_positions, + ticktext=self._model.ticks_labels, ) ) - self.fig["layout"]["yaxis"].update(range=[min(y[:, 0]), max(y[:, 0])]) - # Create and show figure - super().__init__( - final_xspectra, - **kwargs, + self.fig["layout"]["yaxis"].update( + title="meV", + range=[min(self._model.y[:, 0]), max(self._model.y[:, 0])], + ) + + if self.fig.layout.images: + for image in self.fig.layout.images: + image["scl"] = 2 # Set the scale for each image + + self.fig.update_layout( + height=500, + width=700, + margin=dict(l=15, r=15, t=15, b=15), ) + # Update x-axis and y-axis to enable autoscaling + self.fig.update_xaxes(autorange=True) + self.fig.update_yaxes(autorange=True) - def _update_spectra(self, spectra): - ( - final_xspectra, - final_zspectra, - ticks_positions, - ticks_labels, - ) = generated_curated_data(spectra) - # Data to contour is the sum of two Gaussian functions. - x, y = np.meshgrid(spectra[0].x_data.magnitude, spectra[0].y_data.magnitude) - - # If I do this - # self.data = () - # I have a delay in the plotting, I have blank plot while it - # is adding the new trace (see below); So, I will instead do the - # re-assignement of the self.data = [self.data[1]] afterwards. + # Update the layout to enable autoscaling + self.fig.update_layout(autosize=True) + + def _update_plot(self): + # update the spectra, i.e. the data contained in the _model. + self._model._update_spectra() x = None # if mode == "intensity" else x[0] self.fig.add_trace( go.Heatmap( - z=final_zspectra.T, + z=self._model.final_zspectra.T, y=( - y[:, 0] * self.THz_to_meV + self._model.y[:, 0] * self.THz_to_meV if self.E_units_button.value == "meV" - else y[:, 0] + else self._model.y[:, 0] ), - x=x, + x=x, # self._model.x, colorbar=COLORBAR_DICT, colorscale=COLORSCALE, # imported from euphonic_base_widgets ) @@ -106,17 +96,24 @@ def _update_spectra(self, spectra): # this is delays things self.fig.update_layout( xaxis=dict( - tickmode="array", tickvals=ticks_positions, ticktext=ticks_labels + tickmode="array", + tickvals=self._model.ticks_positions, + ticktext=self._model.ticks_labels, ) ) self.fig.data = [self.fig.data[1]] - super()._update_spectra(final_zspectra) + super()._update_plot() class SingleCrystalSettingsWidget(StructureFactorSettingsBaseWidget): - def __init__(self, **kwargs): + def render(self): + if self.rendered: + return + + super().render() + self.custom_kpath_description = ipw.HTML( """
@@ -124,7 +121,7 @@ def __init__(self, **kwargs): we can provide it via a specific format:
(1) each linear path should be divided by '|';
(2) each path is composed of 'qxi qyi qzi - qxf qyf qzf' where qxi and qxf are, respectively, - the start and end x-coordinate of the q direction, in crystal coordinates.
+ the start and end x-coordinate of the q direction, in reciprocal lattice units (rlu).
An example path is: '0 0 0 - 1 1 1 | 1 1 1 - 0.5 0.5 0.5'.
For now, we do not support fractions (i.e. we accept 0.5 but not 1/2).
@@ -139,14 +136,12 @@ def __init__(self, **kwargs): custom_style = '' display(ipw.HTML(custom_style)) self.custom_kpath_text.add_class("custom-font") - + ipw.link( + (self._model, "custom_kpath"), + (self.custom_kpath_text, "value"), + ) self.custom_kpath_text.observe(self._on_setting_changed, names="value") - # Please note: if you change the order of the widgets below, it will - # affect the usage of the children[0] below in the full widget. - - super().__init__() - self.children = [ ipw.HBox( [ @@ -159,10 +154,10 @@ def __init__(self, **kwargs): self.download_button, ] ), - self.float_q_spacing, - self.float_energy_broadening, - self.int_energy_bins, - self.float_T, + self.q_spacing, + self.energy_broadening, + self.energy_bins, + self.temperature, self.weight_button, ], layout=ipw.Layout( @@ -182,108 +177,71 @@ def __init__(self, **kwargs): ), ] - def _reset_settings(self, _): - self.custom_kpath_text.value = "" - super()._reset_settings(_) - class SingleCrystalFullWidget(ipw.VBox): + # I need to put the model also HERE! Think how we can so all of this in simpler way. """ The Widget to display specifically the Intensity map of Dynamical structure - factor for single crystal. + factor for single crystal. It is composed of the following widgets: + - title_intensity: HTML widget with the title of the widget. + - settings_intensity: SingleCrystalSettingsWidget widget with the settings for the plot. + - map_widget: SingleCrystalPlotWidget widget with the plot of the intensity map. + - download_button: Button widget to download the intensity map. + The scattering lengths used in the `produce_bands_weigthed_data` function are tabulated (Euphonic/euphonic/data/sears-1992.json) and are from Sears (1992) Neutron News 3(3) pp26--37. """ - def __init__(self, fc, q_path, **kwargs): - self.fc = fc - self.q_path = q_path + def __init__(self, model): + self._model = model + self.rendered = False + super().__init__() - self.spectra, self.parameters = produce_bands_weigthed_data( - fc=self.fc, - linear_path=self.q_path, - plot=False, # CHANGED - ) + def render(self): + if self.rendered: + return self.title_intensity = ipw.HTML( "

Neutron dynamic structure factor - Single Crystal

" ) - self.settings_intensity = SingleCrystalSettingsWidget() - 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 = SingleCrystalSettingsWidget(model=self._model) + self.map_widget = SingleCrystalPlotWidget(model=self._model) - # This is used in order to have an overall intensity scale. - self.intensity_ref_0K = np.max(self.spectra[0].z_data.magnitude) # CHANGED + # rendering the widgets + self.settings_intensity.render() + self.map_widget.render() - self.map_widget = SingleCrystalPlotWidget( - self.spectra, intensity_ref_0K=self.intensity_ref_0K - ) # CHANGED + # 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_spectra(self.spectra) # CHANGED + self.map_widget._update_plot() def download_data(self, _=None): """ Download both the ForceConstants and the spectra json files. + TODO: improve the format, should be easy to open somewhere else. """ force_constants_dict = self.fc.to_dict() filename = "single_crystal.json" my_dict = {} - for branch in range(len(self.spectra)): - my_dict[str(branch)] = self.spectra[branch].to_dict() - my_dict.update( - { - "weighting": self.settings_intensity.weight_button.value, - "q_spacing": self.settings_intensity.float_q_spacing.value, - "energy_broadening": self.settings_intensity.float_energy_broadening.value, - "ebins": self.settings_intensity.int_energy_bins.value, - "temperature": self.settings_intensity.float_T.value, - } - ) + my_dict["x"] = self._model.final_xspectra.tolist() + my_dict["y"] = self._model.y.tolist() + my_dict["z"] = self._model.final_zspectra.tolist() + my_dict.update(self._model.get_model_state()) for k in ["weighting", "q_spacing", "temperature"]: filename += "_" + k + "_" + str(my_dict[k]) @@ -305,27 +263,6 @@ def download_data(self, _=None): b64_str = base64.b64encode(image_bytes).decode() self._download(payload=b64_str, filename=filename + ".png") - def curate_path_and_labels( - self, - ): - # I do not like this implementation (MB) - coordinates = [] - labels = [] - path = self.settings_intensity.custom_kpath_text.value - linear_paths = path.split("|") - for i in linear_paths: - scoords = [] - s = i.split( - " - " - ) # not i.split("-"), otherwise also the minus of the negative numbers are used for the splitting. - for k in s: - labels.append(k.strip()) - # AAA missing support for fractions. - l = tuple(map(float, [kk for kk in k.strip().split(" ")])) # noqa: E741 - scoords.append(l) - coordinates.append(scoords) - return coordinates, labels - @staticmethod def _download(payload, filename): from IPython.display import Javascript