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