From 0a10129582f01993aa1b29e96198ef8ae77c9813 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 9 Dec 2024 17:34:39 +0000 Subject: [PATCH] Tabs introduction. Missing several things: - energy units conversion - qplanes - downloads. --- .../app/widgets/euphonicmodel.py | 39 ++++++- .../app/widgets/euphonicwidget.py | 12 +- .../app/widgets/structurefactorwidget.py | 110 ++++++++++++------ 3 files changed, 119 insertions(+), 42 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 2617e77..bed3d80 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -52,6 +52,11 @@ class EuphonicResultsModel(Model): energy_bins = tl.Int(200) temperature = tl.Float(0) weighting = tl.Unicode("coherent") + energy_units = tl.Unicode("meV") + intensity_filter = tl.List(trait=tl.Float(), default_value=[0, 100]) + + THz_to_meV = 4.13566553853599 # conversion factor. + THz_to_cm1 = 33.3564095198155 # conversion factor. def __init__(self, node=None, spectrum_type: str = "single_crystal", **kwargs): super().__init__(**kwargs) @@ -193,11 +198,13 @@ def get_spectra( ) = generated_curated_data(spectra) self.z = final_zspectra.T - self.y = self.y[:, 0] - self.x = None # we have the ticks positions and labels + self.y = self.y[:, 0] * self.energy_conversion_factor( + self.energy_units, "meV" + ) + # self.x = None # we have, instead, the ticks positions and labels self.xlabel = "" - self.ylabel = "Energy (meV)" + self.ylabel = f"Energy ({self.energy_units})" elif self.spectrum_type == "powder": # powder case # Spectrum2D as output of the powder data @@ -207,8 +214,10 @@ def get_spectra( # we don't need to curate the powder data, at variance with the single crystal case. # We can directly use them: - self.x = spectra.x_data.magnitude[0] - self.y = self.y[:, 0] + self.x = spectra.x_data.magnitude + self.y = self.y[:, 0] * self.energy_conversion_factor( + self.energy_units, "meV" + ) self.z = spectra.z_data.magnitude.T else: raise ValueError("Spectrum type not recognized:", self.spectrum_type) @@ -261,6 +270,26 @@ def _get_qsection_spectra( self.xlabel = "AAA" self.ylabel = "AAA" + def energy_conversion_factor(self, new, old): + # TODO: check this is correct. + if new == old: + return 1 + if new == "meV": + if old == "THz": + return self.THz_to_meV + elif old == "cm-1": + return self.THz_to_meV * self.THz_to_cm1 + elif new == "THz": + if old == "meV": + return 1 / self.THz_to_meV + elif old == "cm-1": + return 1 / self.THz_to_cm1 + elif new == "cm-1": + if old == "meV": + return 1 / self.THz_to_meV * self.THz_to_cm1 + elif old == "THz": + return self.THz_to_cm1 + def _curate_path_and_labels( self, ): diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py index a222f9f..b5d47c5 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py @@ -118,8 +118,8 @@ def _render_for_real(self, change=None): self.loading_widget.layout.display = "block" self._model.fetch_data() # should be in the model, but I can do it here once for all and then clone the model. - powder_model = EuphonicResultsModel(spectum_type="powder") - qsection_model = EuphonicResultsModel(spectum_type="q_planes") + powder_model = EuphonicResultsModel(spectrum_type="powder") + qsection_model = EuphonicResultsModel(spectrum_type="q_planes") for data in ["fc", "q_path"]: setattr(powder_model, data, getattr(self._model, data)) @@ -133,8 +133,12 @@ def _render_for_real(self, change=None): self.tab_widget.children = ( singlecrystalwidget, - # EuphonicStructureFactorWidget(node=self._model.vibro, model=powder_model, spectrum_type="powder"), - # EuphonicStructureFactorWidget(node=self._model.vibro, model=qsection_model, spectrum_type="q_planes"), + EuphonicStructureFactorWidget( + node=self._model.vibro, model=powder_model, spectrum_type="powder" + ), + EuphonicStructureFactorWidget( + node=self._model.vibro, model=qsection_model, spectrum_type="q_planes" + ), ) for widget in self.tab_widget.children: diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py index 1fee1f1..7759911 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -57,7 +57,12 @@ def render(self): width="400px", ), ) + ipw.link( + (slider_intensity, "value"), + (self._model, "intensity_filter"), + ) slider_intensity.observe(self._update_intensity_filter, "value") + specification_intensity = ipw.HTML( "(Intensity is relative to the maximum intensity at T=0K)" ) @@ -75,6 +80,10 @@ def render(self): width="auto", ), ) + ipw.link( + (E_units_button, "value"), + (self._model, "energy_units"), + ) 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. @@ -113,7 +122,7 @@ def render(self): ) energy_bins.observe(self._on_setting_change, names="value") - temperature = ipw.FloatText( + self.temperature = ipw.FloatText( value=self._model.temperature, step=0.01, description="T (K)", @@ -121,9 +130,9 @@ def render(self): ) ipw.link( (self._model, "temperature"), - (temperature, "value"), + (self.temperature, "value"), ) - temperature.observe(self._on_setting_change, names="value") + self.temperature.observe(self._on_setting_change, names="value") weight_button = ipw.ToggleButtons( options=[ @@ -141,14 +150,14 @@ def render(self): ) weight_button.observe(self._on_weight_button_change, names="value") - plot_button = ipw.Button( + self.plot_button = ipw.Button( description="Replot", icon="pencil", button_style="primary", disabled=True, layout=ipw.Layout(width="auto"), ) - plot_button.observe(self._on_plot_button_change, names="disabled") + self.plot_button.on_click(self._update_plot) reset_button = ipw.Button( description="Reset", @@ -159,14 +168,19 @@ def render(self): ) reset_button.on_click(self._reset_settings) - download_button = ipw.Button( + self.download_button = ipw.Button( description="Download Data and Plot", icon="download", button_style="primary", disabled=False, # Large files... layout=ipw.Layout(width="auto"), ) - download_button.on_click(self._download_data) + self.download_button.on_click(self._download_data) + ipw.dlink( + (self.plot_button, "disabled"), + (self.download_button, "disabled"), + lambda x: not x, + ) self.children += ( ipw.HBox( @@ -183,11 +197,11 @@ def render(self): q_spacing, energy_broadening, energy_bins, - temperature, + self.temperature, weight_button, - plot_button, + self.plot_button, reset_button, - download_button, + self.download_button, ) if self._model.spectrum_type == "single_crystal": @@ -256,6 +270,12 @@ def render(self): (self.int_npts, "value"), ) self.int_npts.observe(self._on_setting_change, names="value") + self.children += ( + self.qmin, + self.qmax, + self.int_npts, + ) + # fi self._model.spectrum_type == "powder" elif self._model.spectrum_type == "q_planes": self.ecenter = ipw.FloatText( @@ -340,9 +360,18 @@ def render(self): self.plot_button.description = "Plot" # self.reset_button.disabled = True self.download_button.disabled = True + + self.children += ( + self.ecenter, + self.plane_description_widget, + self.Q0_widget, + self.h_widget, + self.k_widget, + self.energy_broadening, + ) # fi self._model.spectrum_type == "q_planes" - self.children += (self.fig,) + self.children += (self.figure_container,) self.rendered = True @@ -350,11 +379,9 @@ def _init_view(self, _=None): self._model.fetch_data() if not hasattr(self, "fig"): self.fig = go.FigureWidget() + self.figure_container = ipw.VBox([self.fig]) self._update_plot() - def _on_plot_button_change(self, change): - self.download_button.disabled = not change["new"] - def _on_weight_button_change(self, change): self._model.temperature = 0 self.temperature.disabled = True if change["new"] == "dos" else False @@ -365,7 +392,7 @@ def _on_setting_change( ): # think if we want to do something more evident... self.plot_button.disabled = False - def _update_plot(self): + def _update_plot(self, _=None): # update the spectra, i.e. the data contained in the _model. # TODO: we need to treat differently the update of intensity and units. # they anyway need to modify the data, but no additional spectra re-generation is really needed. @@ -373,16 +400,20 @@ def _update_plot(self): self._model.get_spectra() if not self.rendered: + if self._model.spectrum_type == "q_planes": + # hide figure until we have the data + self.figure_container.layout.display = "none" + # First time we render, we set several layout settings. # Layout settings - if self._model.x: + if hasattr(self._model, "x"): self.fig["layout"]["xaxis"].update( title=self._model.xlabel, - range=[min(self._model.x), max(self._model.x)], + range=[np.min(self._model.x), np.max(self._model.x)], ) self.fig["layout"]["yaxis"].update( title=self._model.ylabel, - range=[min(self._model.y), max(self._model.y)], + range=[np.min(self._model.y), np.max(self._model.y)], ) if self.fig.layout.images: @@ -400,6 +431,8 @@ def _update_plot(self): # Update the layout to enable autoscaling self.fig.update_layout(autosize=True) + elif self._model.spectrum_type == "q_planes" and self.rendered: + self.figure_container.layout.display = "block" heatmap_trace = go.Heatmap( z=self._model.z, @@ -430,26 +463,37 @@ def _update_plot(self): self.fig.add_trace(heatmap_trace) self.fig.data = [self.fig.data[-1]] - def _update_intensity_filter(self, change): + if self.rendered: + self._update_intensity_filter() + + def _update_intensity_filter(self): # the value of the intensity slider is in fractions of the max. - if change["new"] != change["old"]: - self.fig.data[0].zmax = ( - change["new"][1] * np.max(self.fig.data[0].z) / 100 - ) # above this, it is all yellow, i.e. max intensity. - self.fig.data[0].zmin = ( - change["new"][0] * np.max(self.fig.data[0].z) / 100 - ) # below this, it is all blue, i.e. zero intensity + # NOTE: we do this here, as we do not want to replot. + self.fig.data[0].zmax = ( + self._model.intensity_filter[1] * np.max(self.fig.data[0].z) / 100 + ) # above this, it is all yellow, i.e. max intensity. + self.fig.data[0].zmin = ( + self._model.intensity_filter[0] * np.max(self.fig.data[0].z) / 100 + ) # below this, it is all blue, i.e. zero intensity def _update_energy_units(self, change): # the value of the intensity slider is in fractions of the max. - if change["new"] != change["old"]: - self.fig.data[0].y = ( - self.fig.data[0].y * self.THz_to_meV - if change["new"] == "meV" - else self.fig.data[0].y / self.THz_to_meV - ) + self._model.energy_units = change["new"] + self.fig.data[0].y = ( + np.array(self.fig.data[0].y) + * self._model.energy_conversion_factor( + new=self._model.energy_units, old=change["old"] + ), + ) + + self.fig["layout"]["yaxis"].update(title=self._model.energy_units) + + # Update x-axis and y-axis to enable autoscaling + self.fig.update_xaxes(autorange=True) + self.fig.update_yaxes(autorange=True) - self.fig["layout"]["yaxis"].update(title=change["new"]) + # Update the layout to enable autoscaling + self.fig.update_layout(autosize=True) def _reset_settings(self, _): self._model.reset()