From f25621163088c2196e5b0a779c6aa62162e7be85 Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Sun, 8 Dec 2024 22:16:12 +0000 Subject: [PATCH 01/16] Refactoring Euphonic MVC. unification of plot and settings widgets, as well as polymorphism instead of inheritance and composition. This will help maintaining the code clear and readable. This is still a draft commit, let's say. I started doing the unification in the euphonicmodel.py and in the structurefactorwidget.py - but the files, imports and everything need to be fixed --- .../app/widgets/euphonicmodel.py | 143 +++--- .../{data_manipulation => data}/bands_pdos.py | 2 +- .../data/export_vibronic_to_euphonic.py | 41 ++ .../utils/euphonic/data/parameters.py | 52 ++ .../utils/euphonic/data/phonopy_interface.py | 118 +++++ .../structure_factors.py} | 232 +-------- .../utils/euphonic/structurefactorwidget.py | 458 ++++++++++++++++++ 7 files changed, 759 insertions(+), 287 deletions(-) rename src/aiidalab_qe_vibroscopy/utils/euphonic/{data_manipulation => data}/bands_pdos.py (98%) create mode 100644 src/aiidalab_qe_vibroscopy/utils/euphonic/data/export_vibronic_to_euphonic.py create mode 100644 src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py create mode 100644 src/aiidalab_qe_vibroscopy/utils/euphonic/data/phonopy_interface.py rename src/aiidalab_qe_vibroscopy/utils/euphonic/{data_manipulation/intensity_maps.py => data/structure_factors.py} (71%) create mode 100644 src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 687b838..b2f5616 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -33,12 +33,14 @@ class EuphonicBaseResultsModel(Model): # 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 + spectrum_type = "single_crystal" + x_label = None + y_label = "Energy (meV)" + detached_app = False # Settings for single crystal and powder average q_spacing = tl.Float(0.01) @@ -46,6 +48,27 @@ class EuphonicBaseResultsModel(Model): energy_bins = tl.Int(200) temperature = tl.Float(0) weighting = tl.Unicode("coherent") + + 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 fetch_data(self): """Fetch the data from the database or from the uploaded files.""" @@ -65,18 +88,20 @@ def fetch_data(self): def _inject_single_crystal_settings( self, ): - self.parameters = copy.deepcopy( - par_dict - ) # need to be different if powder or q section. + # Case in which we want to inject the model into the single crystal widget + # we define specific parameters dictionary and callback function for the single crystal case + self.parameters = copy.deepcopy(parameters_single_crystal) 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.parameters = copy.deepcopy(parameters_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)) @@ -96,85 +121,77 @@ def _inject_qsection_settings( 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( + def get_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() + + if self.spectrum_type == "q_planes": + self._get_qsection_spectra() + return 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() + coordinates, labels = self._curate_path_and_labels() qpath = { "coordinates": coordinates, "labels": labels, # ["$\Gamma$","X","X","(1,1,1)"], - "delta_q": parameters_["q_spacing"], + "delta_q": self.parameters["q_spacing"], } else: qpath = copy.deepcopy(self.q_path) if qpath: - qpath["delta_q"] = parameters_["q_spacing"] + qpath["delta_q"] = self.parameters["q_spacing"] spectra, parameters = self._callback_spectra_generation( - params=parameters_, + params=AttrDict(self.parameters), fc=self.fc, linear_path=qpath, plot=False, ) # curated spectra (labels and so on...) - if hasattr(self, "custom_kpath"): # single crystal case + if spectrum_type == "single_crystal": # 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, + final_xspectra, + final_zspectra, self.ticks_positions, self.ticks_labels, ) = generated_curated_data(spectra) - else: + + self.z = final_zspectra.T + self.y = self.y[:,0] + self.x = None # we have the ticks positions and labels + + self.xlabel = "" + self.ylabel = "Energy (meV)" + + elif spectrum_type == "powder": # powder case # 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 + # 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.z = spectra.z_data.magnitude.T - def _update_qsection_spectra( + def _get_qsection_spectra( self, ): - parameters_ = AttrDict( + # This is used to update the spectra in the case we plot the Q planes (the third tab). + parameters_qplanes = AttrDict( { "h": np.array([i for i in self.h_vec[:-2]]), "k": np.array([i for i in self.k_vec[:-2]]), @@ -193,32 +210,32 @@ def _update_qsection_spectra( 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, + h=parameters_qplanes.h, + k=parameters_qplanes.k, + Q0=parameters_qplanes.Q0, + n_h=parameters_qplanes.n_h, + n_k=parameters_qplanes.n_k, + h_extension=parameters_qplanes.h_extension, + k_extension=parameters_qplanes.k_extension, + temperature=parameters_qplanes.temperature, ) - self.av_spec, self.q_array, self.h_array, self.k_array, self.labels = ( + self.av_spec, self.z, self.x, self.y, 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, + ecenter=parameters_qplanes.ecenter, + deltaE=parameters_qplanes.deltaE, + bins=parameters_qplanes.bins, + spectrum_type=parameters_qplanes.spectrum_type, dw=dw, labels=labels, ) ) - def curate_path_and_labels( + def _curate_path_and_labels( self, ): # This is used to curate the path and labels of the spectra if custom kpath is provided. @@ -240,9 +257,6 @@ def curate_path_and_labels( 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 @@ -250,3 +264,12 @@ def produce_phonopy_files(self): self.node.phonon_bands.creator, mode="download" ) return phonopy_yaml, fc_hdf5 + + def prepare_data_for_download(self): + raise NotImplementedError("Need to implement the download of a CSV file") + + def _clone(self): + # in case we want to clone the model. + # This is the case when we have the same data and we inject in three + # different models: we don't need to fetch three times. + return copy.deepcopy(self) \ No newline at end of file diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/bands_pdos.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/bands_pdos.py similarity index 98% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/bands_pdos.py rename to src/aiidalab_qe_vibroscopy/utils/euphonic/data/bands_pdos.py index deaafce..58a0e80 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/bands_pdos.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/bands_pdos.py @@ -21,7 +21,7 @@ (from euphonic, using the force constants instances as obtained from phonopy.yaml). These are then used in the widgets to plot the corresponding quantities. -NB: no more used in the QE-app. We use phonopy instead. +NB: not used in the QE-app. We use phonopy instead. """ ######################## diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/export_vibronic_to_euphonic.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/export_vibronic_to_euphonic.py new file mode 100644 index 0000000..7254e93 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/export_vibronic_to_euphonic.py @@ -0,0 +1,41 @@ +from aiida.orm import Dict +from aiida_phonopy.common.raw_parsers import get_force_constants_from_phonopy +from aiida_phonopy.workflows.phonon import generate_force_constant_instance + +def export_euphonic_data(output_vibronic, fermi_energy=None): + if "phonon_bands" not in output_vibronic: + return None + + output_set = output_vibronic.phonon_bands + + 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"] + + grouped_bands = [ + item + for sublist in [band_labels[i : i + 2] for i in range(len(band_labels) - 1)] + for item in sublist + ] + grouped_q = [ + [tuple(band[i : i + 3]), tuple(band[i + 3 : i + 6])] + for i in range(0, len(band) - 3, 3) + ] + q_path = { + "coordinates": grouped_q, + "labels": grouped_bands, + "delta_q": 0.01, # 1/A + } + else: + q_path = None + + phonopy_calc = output_set.creator + fc = generate_force_constant_instance(phonopy_calc) + # bands = compute_bands(fc) + # pdos = compute_pdos(fc) + return { + "fc": fc, + "q_path": q_path, + } # "bands": bands, "pdos": pdos, "thermal": None} diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py new file mode 100644 index 0000000..4db6364 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py @@ -0,0 +1,52 @@ +"""Set of parameters for given Euphonic calculation. + +We distinguish between parameters for a single crystal and for a powder calculation: the former requires a path in reciprocal space, +while the latter requires a range of q-points. +We have a set of common parameters that are shared between the two types of calculations. +""" +common_parameters = { + "weighting": "coherent", # Spectral weighting to plot: DOS, coherent inelastic neutron scattering (default: dos) + "grid": None, # FWHM of broadening on q axis in 1/LENGTH_UNIT (no broadening if unspecified). (default: None) + "grid_spacing": 0.1, # q-point spacing of Monkhorst-Pack grid. (default: 0.1) + "energy_unit": "THz", + "temperature": 0, # Temperature in K; enable Debye-Waller factor calculation. (Only applicable when --weighting=coherent). (default: None) + "shape": "gauss", # The broadening shape (default: gauss) + "length_unit": "angstrom", + "q_spacing": 0.01, # Target distance between q-point samples in 1/LENGTH_UNIT (default: 0.025) + "energy_broadening": 1, + "q_broadening": None, # FWHM of broadening on q axis in 1/LENGTH_UNIT (no broadening if unspecified). (default: None) + "ebins": 200, # Number of energy bins (default: 200) + "e_min": 0, + "e_max": None, + "title": None, + "ylabel": "THz", + "xlabel": "", + "save_json": False, + "no_base_style": False, + "style": False, + "vmin": None, + "vmax": None, + "save_to": None, + "asr": None, # Apply an acoustic-sum-rule (ASR) correction to the data: "realspace" applies the correction to the force constant matrix in real space. "reciprocal" applies the correction to the dynamical matrix at each q-point. (default: None) + "dipole_parameter": 1.0, # Set the cutoff in real/reciprocal space for the dipole Ewald sum; higher values use more reciprocal terms. If tuned correctly this can result in performance improvements. See euphonic-optimise-dipole-parameter program for help on choosing a good DIPOLE_PARAMETER. (default: 1.0) + "use_c": None, + "n_threads": None, +} + +parameters_single_crystal = { + **common_parameters, +} + +parameters_powder = { + **common_parameters, + "q_min": 0, + "q_max": 1, + "npts": 150, + "npts_density": None, + "pdos": None, + "e_i": None, + "sampling": "golden", + "jitter": True, + "e_f": None, + "disable_widgets": True, +} diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/phonopy_interface.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/phonopy_interface.py new file mode 100644 index 0000000..048257f --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/phonopy_interface.py @@ -0,0 +1,118 @@ +import tempfile +import pathlib +import base64 +from typing import Optional +import euphonic +from euphonic.io.phonopy import write_force_constants_to_hdf5 + +def generate_force_constant_instance( + phonopy_calc=None, + path: str = None, + summary_name: str = None, + born_name: Optional[str] = None, + fc_name: str = "FORCE_CONSTANTS", + fc_format: Optional[str] = None, + mode="stream", # "download" to have the download of phonopy.yaml and fc.hdf5 . TOBE IMPLEMENTED. +): + """ + Basically allows to obtain the ForceConstants instance from phonopy, both via files (from the second + input parameters we have the same one of `euphonic.ForceConstants.from_phonopy`), or via a + PhonopyCalculation instance. Respectively, the two ways will support independent euphonic app and integration + of Euphonic into aiidalab. + """ + blockPrint() + + ####### This is done to support the detached app (from aiidalab) with the same code: + if path and summary_name: + fc = euphonic.ForceConstants.from_phonopy( + path=path, + summary_name=summary_name, + fc_name=fc_name, + ) + return fc + elif not phonopy_calc: + raise NotImplementedError( + "Please provide or the files or the phonopy calculation node." + ) + + ####### This is almost copied from PhonopyCalculation and is done to support functionalities in aiidalab env: + from phonopy.interface.phonopy_yaml import PhonopyYaml + + kwargs = {} + + if "settings" in phonopy_calc.inputs: + the_settings = phonopy_calc.inputs.settings.get_dict() + for key in ["symmetrize_nac", "factor_nac", "subtract_residual_forces"]: + if key in the_settings: + kwargs.update({key: the_settings[key]}) + + if "phonopy_data" in phonopy_calc.inputs: + ph = phonopy_calc.inputs.phonopy_data.get_phonopy_instance(**kwargs) + p2s_map = phonopy_calc.inputs.phonopy_data.get_cells_mappings()["primitive"][ + "p2s_map" + ] + ph.produce_force_constants() + elif "force_constants" in phonopy_calc.inputs: + ph = phonopy_calc.inputs.force_constants.get_phonopy_instance(**kwargs) + p2s_map = phonopy_calc.inputs.force_constants.get_cells_mappings()["primitive"][ + "p2s_map" + ] + ph.force_constants = phonopy_calc.inputs.force_constants.get_array( + "force_constants" + ) + + ####### + + # Create temporary directory + # + with tempfile.TemporaryDirectory() as dirpath: + # phonopy.yaml generation: + phpy_yaml = PhonopyYaml() + phpy_yaml.set_phonon_info(ph) + phpy_yaml_txt = str(phpy_yaml) + + with open( + pathlib.Path(dirpath) / "phonopy.yaml", "w", encoding="utf8" + ) as handle: + handle.write(phpy_yaml_txt) + + # Force constants hdf5 file generation: + # all this is needed to load the euphonic instance, in case no FC are written in phonopy.yaml + # which is the case + + write_force_constants_to_hdf5( + force_constants=ph.force_constants, + filename=pathlib.Path(dirpath) / "fc.hdf5", + p2s_map=p2s_map, + ) + + # Here below we trigger the download mode. Can be improved avoiding the repetitions of lines + if mode == "download": + with open( + pathlib.Path(dirpath) / "phonopy.yaml", "r", encoding="utf8" + ) as handle: + file_content = handle.read() + phonopy_yaml_bitstream = base64.b64encode(file_content.encode()).decode( + "utf-8" + ) + + with open( + pathlib.Path(dirpath) / "fc.hdf5", + "rb", + ) as handle: + file_content = handle.read() + fc_hdf5_bitstream = base64.b64encode(file_content).decode() + + return phonopy_yaml_bitstream, fc_hdf5_bitstream + + # Read force constants (fc.hdf5) and summary+NAC (phonopy.yaml) + + fc = euphonic.ForceConstants.from_phonopy( + path=dirpath, + summary_name="phonopy.yaml", + fc_name="fc.hdf5", + ) + # print(filename) + # print(dirpath) + enablePrint() + return fc \ No newline at end of file diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py similarity index 71% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py rename to src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py index 66095ca..edb65d9 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data_manipulation/intensity_maps.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py @@ -85,6 +85,8 @@ def __init__(self, *args, **kwargs): and the powder maps(from euphonic, using the force constants instances as obtained from phonopy.yaml). These are then used in the widgets to plot the corresponding quantities. +NOTE: the two main functions here are produce_bands_weigthed_data and produce_powder_data. + PLEASE NOTE: the scattering lengths are tabulated (Euphonic/euphonic/data/sears-1992.json) and are from Sears (1992) Neutron News 3(3) pp26--37. """ @@ -174,38 +176,6 @@ def join_q_paths(coordinates: list, labels: list, delta_q=0.1, G=[0, 0, 0]): ################################ START INTENSITY PLOT GENERATOR ######################## -par_dict = { - "weighting": "coherent", # Spectral weighting to plot: DOS, coherent inelastic neutron scattering (default: dos) - "grid": None, # FWHM of broadening on q axis in 1/LENGTH_UNIT (no broadening if unspecified). (default: None) - "grid_spacing": 0.1, # q-point spacing of Monkhorst-Pack grid. (default: 0.1) - "energy_unit": "THz", - "temperature": 0, # Temperature in K; enable Debye-Waller factor calculation. (Only applicable when --weighting=coherent). (default: None) - #'btol':, - "shape": "gauss", # The broadening shape (default: gauss) - "length_unit": "angstrom", - "q_spacing": 0.01, # Target distance between q-point samples in 1/LENGTH_UNIT (default: 0.025) - "energy_broadening": 1, - "q_broadening": None, # FWHM of broadening on q axis in 1/LENGTH_UNIT (no broadening if unspecified). (default: None) - "ebins": 200, # Number of energy bins (default: 200) - "e_min": 0, - "e_max": None, - "title": None, - "ylabel": "THz", - "xlabel": "", - "save_json": False, - "no_base_style": False, - "style": False, - "vmin": None, - "vmax": None, - "save_to": None, - "asr": None, # Apply an acoustic-sum-rule (ASR) correction to the data: "realspace" applies the correction to the force constant matrix in real space. "reciprocal" applies the correction to the dynamical matrix at each q-point. (default: None) - "dipole_parameter": 1.0, # Set the cutoff in real/reciprocal space for the dipole Ewald sum; higher values use more reciprocal terms. If tuned correctly this can result in performance improvements. See euphonic-optimise-dipole-parameter program for help on choosing a good DIPOLE_PARAMETER. (default: 1.0) - "use_c": None, - "n_threads": None, -} - -parameters = par_dict - def produce_bands_weigthed_data( params: Optional[List[str]] = parameters, @@ -385,48 +355,8 @@ def produce_bands_weigthed_data( ################################ START POWDER ######################## -par_dict_powder = { - "weighting": "coherent", # Spectral weighting to plot: DOS, coherent inelastic neutron scattering (default: dos) - "grid": None, # FWHM of broadening on q axis in 1/LENGTH_UNIT (no broadening if unspecified). (default: None) - "grid_spacing": 0.1, # q-point spacing of Monkhorst-Pack grid. (default: 0.1) - "q_min": 0, - "q_max": 1, - "temperature": None, # Temperature in K; enable Debye-Waller factor calculation. (Only applicable when --weighting=coherent). (default: None) - "ebins": 200, # Number of energy bins (default: 200) - "q_spacing": 0.01, # Target distance between q-point samples in 1/LENGTH_UNIT (default: 0.025) - "energy_broadening": 1, - "npts": 150, - #'grid':, - "energy_unit": "THz", - #'btol':, - "shape": "gauss", # The broadening shape (default: gauss) - "length_unit": "angstrom", - "q_broadening": None, # FWHM of broadening on q axis in 1/LENGTH_UNIT (no broadening if unspecified). (default: None) - "e_min": 0, - "e_max": None, - "title": None, - "ylabel": "THz", - "xlabel": "", - "save_json": False, - "no_base_style": False, - "style": False, - "vmin": None, - "vmax": None, - "save_to": None, - "asr": None, - "dipole_parameter": 1.0, - "use_c": None, - "n_threads": None, - "npts_density": None, - "pdos": None, - "e_i": None, - "sampling": "golden", - "jitter": True, - "e_f": None, - "disable_widgets": True, -} - -parameters_powder = AttrDict(par_dict_powder) + +#parameters_powder = AttrDict(par_dict_powder) def produce_powder_data( @@ -436,6 +366,8 @@ def produce_powder_data( linear_path=None, ) -> None: blockPrint() + """Read the description of the produce_bands_weigthed_data function for more details. + """ # args = get_args(get_parser(), params) if not params: @@ -648,158 +580,6 @@ def update_max(max_val): matplotlib_save_or_show(save_filename=args.save_to) -def generate_force_constant_instance( - phonopy_calc=None, - path: str = None, - summary_name: str = None, - born_name: Optional[str] = None, - fc_name: str = "FORCE_CONSTANTS", - fc_format: Optional[str] = None, - mode="stream", # "download" to have the download of phonopy.yaml and fc.hdf5 . TOBE IMPLEMENTED. -): - """ - Basically allows to obtain the ForceConstants instance from phonopy, both via files (from the second - input parameters we have the same one of `euphonic.ForceConstants.from_phonopy`), or via a - PhonopyCalculation instance. Respectively, the two ways will support independent euphonic app and integration - of Euphonic into aiidalab. - """ - blockPrint() - - ####### This is done to support the detached app (from aiidalab) with the same code: - if path and summary_name: - fc = euphonic.ForceConstants.from_phonopy( - path=path, - summary_name=summary_name, - fc_name=fc_name, - ) - return fc - elif not phonopy_calc: - raise NotImplementedError( - "Please provide or the files or the phonopy calculation node." - ) - - ####### This is almost copied from PhonopyCalculation and is done to support functionalities in aiidalab env: - from phonopy.interface.phonopy_yaml import PhonopyYaml - - kwargs = {} - - if "settings" in phonopy_calc.inputs: - the_settings = phonopy_calc.inputs.settings.get_dict() - for key in ["symmetrize_nac", "factor_nac", "subtract_residual_forces"]: - if key in the_settings: - kwargs.update({key: the_settings[key]}) - - if "phonopy_data" in phonopy_calc.inputs: - ph = phonopy_calc.inputs.phonopy_data.get_phonopy_instance(**kwargs) - p2s_map = phonopy_calc.inputs.phonopy_data.get_cells_mappings()["primitive"][ - "p2s_map" - ] - ph.produce_force_constants() - elif "force_constants" in phonopy_calc.inputs: - ph = phonopy_calc.inputs.force_constants.get_phonopy_instance(**kwargs) - p2s_map = phonopy_calc.inputs.force_constants.get_cells_mappings()["primitive"][ - "p2s_map" - ] - ph.force_constants = phonopy_calc.inputs.force_constants.get_array( - "force_constants" - ) - - ####### - - # Create temporary directory - # - with tempfile.TemporaryDirectory() as dirpath: - # phonopy.yaml generation: - phpy_yaml = PhonopyYaml() - phpy_yaml.set_phonon_info(ph) - phpy_yaml_txt = str(phpy_yaml) - - with open( - pathlib.Path(dirpath) / "phonopy.yaml", "w", encoding="utf8" - ) as handle: - handle.write(phpy_yaml_txt) - - # Force constants hdf5 file generation: - # all this is needed to load the euphonic instance, in case no FC are written in phonopy.yaml - # which is the case - - write_force_constants_to_hdf5( - force_constants=ph.force_constants, - filename=pathlib.Path(dirpath) / "fc.hdf5", - p2s_map=p2s_map, - ) - - # Here below we trigger the download mode. Can be improved avoiding the repetitions of lines - if mode == "download": - with open( - pathlib.Path(dirpath) / "phonopy.yaml", "r", encoding="utf8" - ) as handle: - file_content = handle.read() - phonopy_yaml_bitstream = base64.b64encode(file_content.encode()).decode( - "utf-8" - ) - - with open( - pathlib.Path(dirpath) / "fc.hdf5", - "rb", - ) as handle: - file_content = handle.read() - fc_hdf5_bitstream = base64.b64encode(file_content).decode() - - return phonopy_yaml_bitstream, fc_hdf5_bitstream - - # Read force constants (fc.hdf5) and summary+NAC (phonopy.yaml) - - fc = euphonic.ForceConstants.from_phonopy( - path=dirpath, - summary_name="phonopy.yaml", - fc_name="fc.hdf5", - ) - # print(filename) - # print(dirpath) - enablePrint() - return fc - - -def export_euphonic_data(output_vibronic, fermi_energy=None): - if "phonon_bands" not in output_vibronic: - return None - - output_set = output_vibronic.phonon_bands - - 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"] - - grouped_bands = [ - item - for sublist in [band_labels[i : i + 2] for i in range(len(band_labels) - 1)] - for item in sublist - ] - grouped_q = [ - [tuple(band[i : i + 3]), tuple(band[i + 3 : i + 6])] - for i in range(0, len(band) - 3, 3) - ] - q_path = { - "coordinates": grouped_q, - "labels": grouped_bands, - "delta_q": 0.01, # 1/A - } - else: - q_path = None - - phonopy_calc = output_set.creator - fc = generate_force_constant_instance(phonopy_calc) - # bands = compute_bands(fc) - # pdos = compute_pdos(fc) - return { - "fc": fc, - "q_path": q_path, - } # "bands": bands, "pdos": pdos, "thermal": None} - - def generated_curated_data(spectra): # here we concatenate the bands groups and create the ticks and labels. diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py new file mode 100644 index 0000000..3d9c9bf --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py @@ -0,0 +1,458 @@ +class EuphonicStructureFactorWidget(ipw.VBox): + """Description. + + Collects all the button and widget used to define settings for Neutron dynamic structure factor, + in all the three cases: single crystal, powder, and q-section.... + """ + + def __init__(self, model, spectrum_type = "single_crystal", detached_app = False, **kwargs): + super().__init__() + node + self._model = model + self._model.spectrum_type = spectrum_type + self._model.detached_app = detached_app + self.rendered = False + + def render(self): + """Render the widget. + + This means render the plot button. + """ + if self.rendered: + return + + 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._render_for_real) + + self.loading_widget = LoadingWidget("Loading INS data") + self.loading_widget.layout.display = "none" + + if not self._model.detached_app: + self.plot_button.disabled = False + else: + self.upload_widget = UploadPhonopyWidget() + self.upload_widget.reset_uploads.on_click(self._on_reset_uploads_button_clicked) + 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.download_widget = DownloadYamlHdf5Widget(model=self._model) + self.download_widget.layout.display = "none" + + self.children += ( + self.plot_button, + self.loading_widget, + self.tab_widget, + self.download_widget, + ) + + # NOTE: we initialise here the figure widget, but we do not plot anything yet. + # this is useful to call the init_view method, which contains the update for the figure. + self.fig = go.FigureWidget() + + self.rendered = True + + def _render_for_real(self, change=None): + + self.plot_button.layout.display = "none" + self.loading_widget.layout.display = "block" + + self._init_view() + + slider_intensity = ipw.FloatRangeSlider( + value=[1, 100], # Default selected interval + min=1, + max=100, + step=1, + orientation="horizontal", + readout=True, + readout_format=".0f", + layout=ipw.Layout( + width="400px", + ), + ) + slider_intensity.observe(self._update_intensity_filter, "value") + specification_intensity = ipw.HTML( + "(Intensity is relative to the maximum intensity at T=0K)" + ) + + E_units_button = ipw.ToggleButtons( + options=[ + ("meV", "meV"), + ("THz", "THz"), + # ("cm-1", "cm-1"), + ], + value="meV", + description="Energy units:", + disabled=False, + layout=ipw.Layout( + width="auto", + ), + ) + 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. + + q_spacing = ipw.FloatText( + value=self._model.q_spacing, + step=0.001, + description="q step (1/A)", + tooltip="q spacing in 1/A", + ) + ipw.link( + (self._model, "q_spacing"), + (self.q_spacing, "value"), + ) + q_spacing.observe(self._on_setting_change, names="value") + + energy_broadening = ipw.FloatText( + value=self._model.energy_broadening, + step=0.01, + description="ΔE (meV)", + tooltip="Energy broadening in meV", + ) + ipw.link( + (self._model, "energy_broadening"), + (energy_broadening, "value"), + ) + energy_broadening.observe(self._on_setting_change, names="value") + + energy_bins = ipw.IntText( + value=self._model.energy_bins, + description="#E bins", + tooltip="Number of energy bins", + ) + ipw.link( + (self._model, "energy_bins"), + (energy_bins, "value"), + ) + energy_bins.observe(self._on_setting_change, names="value") + + temperature = ipw.FloatText( + value=self._model.temperature, + step=0.01, + description="T (K)", + disabled=False, + ) + ipw.link( + (self._model, "temperature"), + (temperature, "value"), + ) + temperature.observe(self._on_setting_change, names="value") + + weight_button = ipw.ToggleButtons( + options=[ + ("Coherent", "coherent"), + ("DOS", "dos"), + ], + value=self._model.weighting, + description="weight:", + disabled=False, + style={"description_width": "initial"}, + ) + ipw.link( + (self._model, "weighting"), + (weight_button, "value"), + ) + weight_button.observe(self._on_weight_button_change, names="value") + + 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") + + reset_button = ipw.Button( + description="Reset", + icon="recycle", + button_style="primary", + disabled=False, + layout=ipw.Layout(width="auto"), + ) + reset_button.on_click(self._reset_settings) + + 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) + + if self._model.spectrum_type == "single_crystal": + 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") + ipw.link( + (self._model, "custom_kpath"), + (self.custom_kpath_text, "value"), + ) + self.custom_kpath_text.observe(self._on_setting_changed, names="value") + # fi self._model.spectrum_type == "single_crystal" + elif self._model.spectrum_type == "powder": + self.qmin = ipw.FloatText( + value=0, + description="|q|min (1/A)", + ) + ipw.link( + (self._model, "q_min"), + (self.qmin, "value"), + ) + self.qmin.observe(self._on_setting_changed, names="value") + + self.qmax = ipw.FloatText( + step=0.01, + value=1, + description="|q|max (1/A)", + ) + 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") + # fi self._model.spectrum_type == "powder" + elif self._model.spectrum_type == "q_planes": + self.ecenter = ipw.FloatText( + value=0, + description="E (meV)", + ) + ipw.link( + (self._model, "center_e"), + (self.ecenter, "value"), + ) + self.ecenter.observe(self._on_setting_changed, names="value") + + self.plane_description_widget = ipw.HTML( + """ +
+ Q-plane definition:
+ To define a plane in the reciprocal space,
+ you should define a point in the reciprocal space, Q0, + and two vectors h⃗ and k⃗. Then, each Q point is defined as: Q = Q0 + α*h⃗ + β*k⃗.
+ Then you can select the number of q points in both directions and the α and β parameters.
+ Coordinates are reciprocal lattice units (rlu). +
+ """ + ) + + self.Q0_vec = ipw.HBox( + [ipw.FloatText(value=0, layout={"width": "60px"}) for j in range(3)] + + [ + ipw.HTML( + "Nhq, Nkq ↓", + layout={"width": "60px"}, + ), + ipw.HTML(r"α, β ↓", layout={"width": "60px"}), + ] + ) + + self.h_vec = ipw.HBox( + [ + ipw.FloatText(value=1, layout={"width": "60px"}) # coordinates + for j in range(3) + ] + + [ + ipw.IntText(value=100, layout={"width": "60px"}), + ipw.IntText(value=1, layout={"width": "60px"}), + ] # number of points along this dir, i.e. n_h; and multiplicative factor alpha + ) + self.k_vec = ipw.HBox( + [ipw.FloatText(value=1, layout={"width": "60px"}) for j in range(3)] + + [ + ipw.IntText(value=100, layout={"width": "60px"}), + ipw.IntText(value=1, layout={"width": "60px"}), + ] + ) + + 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] + ) + self.h_widget = ipw.HBox( + [ipw.HTML("h: ", layout={"width": "20px"}), self.h_vec] + ) + self.k_widget = ipw.HBox( + [ipw.HTML("k: ", layout={"width": "20px"}), self.k_vec] + ) + + self.energy_broadening = ipw.FloatText( + value=0.5, + description="ΔE (meV)", + tooltip="Energy window in eV", + ) + 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" + # self.reset_button.disabled = True + self.download_button.disabled = True + # fi self._model.spectrum_type == "q_planes" + + self.children += ( + ... + ) + + def _init_view(self, _=None): + self._model.fetch_data() + 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 + self.plot_button.disabled = False + + def _on_setting_change( + self, change + ): # think if we want to do something more evident... + self.plot_button.disabled = False + + def _update_plot(self): + # 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. + # so the update_spectra need some more logic, or we call another method. + self._model.get_spectra() + + if not self.rendered: + # First time we render, we set several layout settings. + # Layout settings + self.fig["layout"]["xaxis"].update( + title=self._model.xlabel, + range=[min(self._model.x), max(self._model.x)], + ) + self.fig["layout"]["yaxis"].update( + title=self._model.ylabel, + range=[min(self._model.y), max(self._model.y)], + ) + + 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) + + + heatmap_trace = go.Heatmap( + z=self._model.z, + y=(self._model.y), + x=self._model.x, + colorbar=COLORBAR_DICT, + colorscale=COLORSCALE, # imported from euphonic_base_widgets + ) + + # change the path wants also a change in the labels + if "ticks_positions" in self._model and "ticks_labels" in self._model: + self.fig.update_layout( + xaxis=dict( + tickmode="array", + tickvals=self._model.ticks_positions, + ticktext=self._model.ticks_labels, + ) + ) + + # Add colorbar + colorbar = heatmap_trace.colorbar + colorbar.x = 1.05 # Move colorbar to the right + colorbar.y = 0.5 # Center colorbar vertically + + # Add heatmap trace to figure + self.fig.add_trace(heatmap_trace) + self.fig.data = [self.fig.data[1]] + + def _reset_settings(self, _): + self._model.reset() + + def _download_data(self, _=None): + data, filename = self._model.prepare_data_for_download() + self._download(data, filename) + + @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) + + 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] From 8d3a3da0c1fcc76300189c9c4b3a5763f1132fd5 Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Sun, 8 Dec 2024 22:24:21 +0000 Subject: [PATCH 02/16] qplanes mod --- .../app/widgets/euphonicmodel.py | 30 ++++++++++--------- .../utils/euphonic/structurefactorwidget.py | 2 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index b2f5616..f625c3f 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -191,7 +191,7 @@ def _get_qsection_spectra( self, ): # This is used to update the spectra in the case we plot the Q planes (the third tab). - parameters_qplanes = AttrDict( + self.parameters_qplanes = AttrDict( { "h": np.array([i for i in self.h_vec[:-2]]), "k": np.array([i for i in self.k_vec[:-2]]), @@ -210,14 +210,14 @@ def _get_qsection_spectra( modes, q_array, h_array, k_array, labels, dw = produce_Q_section_modes( self.fc, - h=parameters_qplanes.h, - k=parameters_qplanes.k, - Q0=parameters_qplanes.Q0, - n_h=parameters_qplanes.n_h, - n_k=parameters_qplanes.n_k, - h_extension=parameters_qplanes.h_extension, - k_extension=parameters_qplanes.k_extension, - temperature=parameters_qplanes.temperature, + h=self.parameters_qplanes.h, + k=self.parameters_qplanes.k, + Q0=self.parameters_qplanes.Q0, + n_h=self.parameters_qplanes.n_h, + n_k=self.parameters_qplanes.n_k, + h_extension=self.parameters_qplanes.h_extension, + k_extension=self.parameters_qplanes.k_extension, + temperature=self.parameters_qplanes.temperature, ) self.av_spec, self.z, self.x, self.y, self.labels = ( @@ -226,15 +226,17 @@ def _get_qsection_spectra( q_array, h_array, k_array, - ecenter=parameters_qplanes.ecenter, - deltaE=parameters_qplanes.deltaE, - bins=parameters_qplanes.bins, - spectrum_type=parameters_qplanes.spectrum_type, + ecenter=self.parameters_qplanes.ecenter, + deltaE=self.parameters_qplanes.deltaE, + bins=self.parameters_qplanes.bins, + spectrum_type=self.parameters_qplanes.spectrum_type, dw=dw, labels=labels, ) ) - + self.xlabel = "AAA" + self.ylabel = "AAA" + def _curate_path_and_labels( self, ): diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py index 3d9c9bf..acda557 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py @@ -451,7 +451,7 @@ def _download(payload, filename): def _on_vector_changed(self, change=None): """ - Update the model. + Update the model. Specific to qplanes case. """ 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] From 4ac8a33b32b9bd50af38f70c13e1c93b6bd4d37a Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Mon, 9 Dec 2024 10:39:25 +0000 Subject: [PATCH 03/16] more refactoring --- .../app/widgets/euphonicmodel.py | 15 +- .../app/widgets/euphonicwidget.py | 209 +++--------- .../widgets}/structurefactorwidget.py | 58 +--- .../utils/euphonic/data/structure_factors.py | 4 +- .../{ => detached_app}/Detached_app.ipynb | 0 .../{ => detached_app}/static/style.css | 0 .../{ => detached_app}/static/welcome.jinja | 0 .../euphonic/detached_app/uploadwidgets.py | 312 ++++++++++++++++++ 8 files changed, 387 insertions(+), 211 deletions(-) rename src/aiidalab_qe_vibroscopy/{utils/euphonic => app/widgets}/structurefactorwidget.py (88%) rename src/aiidalab_qe_vibroscopy/utils/euphonic/{ => detached_app}/Detached_app.ipynb (100%) rename src/aiidalab_qe_vibroscopy/utils/euphonic/{ => detached_app}/static/style.css (100%) rename src/aiidalab_qe_vibroscopy/utils/euphonic/{ => detached_app}/static/welcome.jinja (100%) create mode 100644 src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index f625c3f..500bd65 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -2,17 +2,21 @@ import traitlets as tl import copy -from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import ( +from aiidalab_qe_vibroscopy.utils.euphonic.data.structure_factors 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.data.parameters import ( + parameters_single_crystal, + parameters_powder, +) + + from aiidalab_qe_vibroscopy.utils.euphonic.tab_widgets.euphonic_q_planes_widgets import ( produce_Q_section_modes, produce_Q_section_spectrum, @@ -21,7 +25,7 @@ from aiidalab_qe.common.mvc import Model -class EuphonicBaseResultsModel(Model): +class EuphonicResultsModel(Model): """Model for the neutron scattering results panel.""" # Here we mode all the model and data-controller, i.e. all the data and their @@ -73,6 +77,9 @@ def reset( def fetch_data(self): """Fetch the data from the database or from the uploaded files.""" # 1. from aiida, so we have the node + if hasattr(self, "fc"): + # we already have the data (this happens if I clone the model with already the data inside) + return if self.node: ins_data = export_euphonic_data(self.node) self.fc = ins_data["fc"] diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py index 2a3ad09..8808fb3 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py @@ -1,117 +1,20 @@ import pathlib import tempfile - - +import ipywidgets as ipw from IPython.display import display -import ipywidgets as ipw +from aiidalab_qe.common.widgets import LoadingWidget -# from ..euphonic.bands_pdos import * -from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import ( +from aiidalab_qe_vibroscopy.utils.euphonic.data.structure_factors 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 +from aiidalab_qe_vibroscopy.app.widgets.structurefactorwidget import EuphonicStructureFactorWidget ##### START OVERALL WIDGET TO DISPLAY EVERYTHING: - -class EuphonicSuperWidget(ipw.VBox): +class EuphonicWidget(ipw.VBox): """ Widget that will include everything, from the upload widget to the tabs with single crystal and powder predictions. @@ -119,7 +22,7 @@ class EuphonicSuperWidget(ipw.VBox): """ def __init__( - self, mode="aiidalab-qe app plugin", model=None, node=None, fc=None, q_path=None + self, model: EuphonicResultsModel, node=None, detached_app = False, **kwargs, ): """ Initialize the Euphonic utility class. @@ -147,23 +50,20 @@ def __init__( fc : optional Force constants if provided. """ - - self.mode = mode + + super().__init__() + self._model = model # this is the single crystal model. - self._model.node = node + if node: self._model.vibro = node + self._model.detached_app = detached_app 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") @@ -178,31 +78,64 @@ def render(self): disabled=True, layout=ipw.Layout(width="auto"), ) - self.plot_button.on_click(self._on_first_plot_button_clicked) + self.plot_button.on_click(self._render_for_real) 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" + if not self._model.detached_app: self.plot_button.disabled = False else: + from aiidalab_qe_vibroscopy.utils.euphonic.detached_app.uploadwidgets import UploadPhonopyWidget + self.upload_widget = UploadPhonopyWidget() + self.upload_widget.reset_uploads.on_click(self._on_reset_uploads_button_clicked) 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.download_widget = DownloadYamlHdf5Widget(model=self._model) self.download_widget.layout.display = "none" - self.children = [ - self.upload_widget, + self.children += ( self.plot_button, - self.loading_widget, self.tab_widget, self.download_widget, - ] - + self.loading_widget, + ) + self.rendered = True + def _render_for_real(self, change=None): + # 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, but I can do it here once for all and then clone 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" + def _on_reset_uploads_button_clicked(self, change): self.upload_widget.upload_phonopy_yaml.value.clear() self.upload_widget.upload_phonopy_yaml._counter = 0 @@ -235,39 +168,7 @@ def _on_upload_hdf5(self, change): 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 diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py similarity index 88% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py rename to src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py index acda557..51857fc 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -1,3 +1,6 @@ +# ADD ALL THE IMPORTS. + + class EuphonicStructureFactorWidget(ipw.VBox): """Description. @@ -5,10 +8,10 @@ class EuphonicStructureFactorWidget(ipw.VBox): in all the three cases: single crystal, powder, and q-section.... """ - def __init__(self, model, spectrum_type = "single_crystal", detached_app = False, **kwargs): + def __init__(self, model, node=None, spectrum_type = "single_crystal", detached_app = False, **kwargs): super().__init__() - node self._model = model + if node: self._model.vibro = node self._model.spectrum_type = spectrum_type self._model.detached_app = detached_app self.rendered = False @@ -21,55 +24,6 @@ def render(self): if self.rendered: return - 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._render_for_real) - - self.loading_widget = LoadingWidget("Loading INS data") - self.loading_widget.layout.display = "none" - - if not self._model.detached_app: - self.plot_button.disabled = False - else: - self.upload_widget = UploadPhonopyWidget() - self.upload_widget.reset_uploads.on_click(self._on_reset_uploads_button_clicked) - 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.download_widget = DownloadYamlHdf5Widget(model=self._model) - self.download_widget.layout.display = "none" - - self.children += ( - self.plot_button, - self.loading_widget, - self.tab_widget, - self.download_widget, - ) - - # NOTE: we initialise here the figure widget, but we do not plot anything yet. - # this is useful to call the init_view method, which contains the update for the figure. - self.fig = go.FigureWidget() - - self.rendered = True - - def _render_for_real(self, change=None): - - self.plot_button.layout.display = "none" - self.loading_widget.layout.display = "block" - self._init_view() slider_intensity = ipw.FloatRangeSlider( @@ -345,6 +299,8 @@ def _render_for_real(self, change=None): self.children += ( ... ) + + self.rendered = True def _init_view(self, _=None): self._model.fetch_data() diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py index edb65d9..6634dd4 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py @@ -178,7 +178,7 @@ def join_q_paths(coordinates: list, labels: list, delta_q=0.1, G=[0, 0, 0]): def produce_bands_weigthed_data( - params: Optional[List[str]] = parameters, + params: Optional[List[str]] = None, fc: ForceConstants = None, linear_path=None, plot=False, @@ -360,7 +360,7 @@ def produce_bands_weigthed_data( def produce_powder_data( - params: Optional[List[str]] = parameters_powder, + params: Optional[List[str]] = None, fc: ForceConstants = None, plot=False, linear_path=None, diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb similarity index 100% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/Detached_app.ipynb rename to src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/static/style.css b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/static/style.css similarity index 100% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/static/style.css rename to src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/static/style.css diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/static/welcome.jinja b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/static/welcome.jinja similarity index 100% rename from src/aiidalab_qe_vibroscopy/utils/euphonic/static/welcome.jinja rename to src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/static/welcome.jinja diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py new file mode 100644 index 0000000..2a3ad09 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py @@ -0,0 +1,312 @@ +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, + ] + + self.rendered = True + + 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) From 27efadbef41353aedfb95d05fb70b0198febc232 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 9 Dec 2024 12:38:19 +0000 Subject: [PATCH 04/16] Adding the model and view to the rendering. Putting init in the model, to inject correct traits. --- .../app/result/result.py | 10 +- .../app/widgets/euphonic/model.py | 2 +- .../app/widgets/euphonicmodel.py | 130 ++++++++----- .../app/widgets/euphonicwidget.py | 63 +++--- .../app/widgets/structurefactorwidget.py | 183 +++++++++++------- .../base_widgets/euphonic_base_widgets.py | 2 +- .../data/export_vibronic_to_euphonic.py | 9 +- .../utils/euphonic/data/phonopy_interface.py | 12 +- .../utils/euphonic/data/structure_factors.py | 20 +- .../euphonic/detached_app/uploadwidgets.py | 12 +- .../tab_widgets/euphonic_q_planes_widgets.py | 4 +- 11 files changed, 272 insertions(+), 175 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/result/result.py b/src/aiidalab_qe_vibroscopy/app/result/result.py index 8ad8d69..9548a2c 100644 --- a/src/aiidalab_qe_vibroscopy/app/result/result.py +++ b/src/aiidalab_qe_vibroscopy/app/result/result.py @@ -13,14 +13,10 @@ 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.euphonicwidget import EuphonicWidget from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import ( - EuphonicBaseResultsModel as EuphonicModel, + EuphonicResultsModel as EuphonicModel, ) -# from aiidalab_qe_vibroscopy.app.widgets.euphonic.model import EuphonicModel -# from aiidalab_qe_vibroscopy.app.widgets.euphonic.widget import EuphonicWidget class VibroResultsPanel(ResultsPanel[VibroResultsModel]): @@ -76,7 +72,7 @@ def render(self): ) tab_data.append(("Dielectric Properties", dielectric_widget)) - needs_euphonic_tab = False # self._model.needs_euphonic_tab() + needs_euphonic_tab = self._model.needs_euphonic_tab() if needs_euphonic_tab: euphonic_model = EuphonicModel() euphonic_widget = EuphonicWidget( diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/model.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/model.py index 264ca43..3859069 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/model.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/model.py @@ -1,7 +1,7 @@ from aiidalab_qe.common.panel import ResultsModel from aiida.common.extendeddicts import AttributeDict import traitlets as tl -from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import ( +from aiidalab_qe_vibroscopy.utils.euphonic.data.export_vibronic_to_euphonic import ( export_euphonic_data, ) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 500bd65..2617e77 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -1,14 +1,21 @@ import numpy as np import traitlets as tl import copy +from IPython.display import display from aiidalab_qe_vibroscopy.utils.euphonic.data.structure_factors import ( AttrDict, produce_bands_weigthed_data, produce_powder_data, generated_curated_data, +) + +from aiidalab_qe_vibroscopy.utils.euphonic.data.phonopy_interface import ( + generate_force_constant_from_phonopy, +) + +from aiidalab_qe_vibroscopy.utils.euphonic.data.export_vibronic_to_euphonic import ( export_euphonic_data, - generate_force_constant_instance, ) from aiidalab_qe_vibroscopy.utils.euphonic.data.parameters import ( @@ -22,6 +29,7 @@ produce_Q_section_spectrum, ) + from aiidalab_qe.common.mvc import Model @@ -38,21 +46,33 @@ class EuphonicResultsModel(Model): # 2. powder average: pa # 3. Q planes: qp - spectra = {} - path = [] - q_path = None - spectrum_type = "single_crystal" - x_label = None - y_label = "Energy (meV)" - detached_app = False - # 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 __init__(self, node=None, spectrum_type: str = "single_crystal", **kwargs): + super().__init__(**kwargs) + + self.spectra = {} + self.path = [] + self.q_path = None + self.spectrum_type = spectrum_type + self.xlabel = None + self.ylabel = "Energy (meV)" + self.detached_app = False + if node: + self.vibro = node + + if self.spectrum_type == "single_crystal": + self._inject_single_crystal_settings() + elif self.spectrum_type == "powder": + self._inject_powder_settings() + elif self.spectrum_type == "q_planes": + self._inject_qsection_settings() + def set_model_state(self, parameters: dict): for k, v in parameters.items(): setattr(self, k, v) @@ -78,10 +98,10 @@ def fetch_data(self): """Fetch the data from the database or from the uploaded files.""" # 1. from aiida, so we have the node if hasattr(self, "fc"): - # we already have the data (this happens if I clone the model with already the data inside) - return - if self.node: - ins_data = export_euphonic_data(self.node) + # we already have the data (this happens if I clone the model with already the data inside) + return + if self.vibro: + ins_data = export_euphonic_data(self.vibro) self.fc = ins_data["fc"] self.q_path = ins_data["q_path"] # 2. from uploaded files... @@ -99,7 +119,7 @@ def _inject_single_crystal_settings( # we define specific parameters dictionary and callback function for the single crystal case self.parameters = copy.deepcopy(parameters_single_crystal) self._callback_spectra_generation = produce_bands_weigthed_data - + # Dynamically add a trait for single crystal settings self.add_traits(custom_kpath=tl.Unicode("")) @@ -108,7 +128,7 @@ def _inject_powder_settings( ): self.parameters = copy.deepcopy(parameters_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)) @@ -132,10 +152,7 @@ def get_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() - + if self.spectrum_type == "q_planes": self._get_qsection_spectra() return @@ -164,7 +181,7 @@ def get_spectra( ) # curated spectra (labels and so on...) - if spectrum_type == "single_crystal": # single crystal case + if self.spectrum_type == "single_crystal": # single crystal case self.x, self.y = np.meshgrid( spectra[0].x_data.magnitude, spectra[0].y_data.magnitude ) @@ -174,15 +191,15 @@ def get_spectra( self.ticks_positions, self.ticks_labels, ) = 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.x = None # we have the ticks positions and labels + self.xlabel = "" self.ylabel = "Energy (meV)" - - elif spectrum_type == "powder": # powder case + + elif self.spectrum_type == "powder": # powder case # Spectrum2D as output of the powder data self.x, self.y = np.meshgrid( spectra.x_data.magnitude, spectra.y_data.magnitude @@ -191,8 +208,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.y = self.y[:, 0] self.z = spectra.z_data.magnitude.T + else: + raise ValueError("Spectrum type not recognized:", self.spectrum_type) def _get_qsection_spectra( self, @@ -227,23 +246,21 @@ def _get_qsection_spectra( temperature=self.parameters_qplanes.temperature, ) - self.av_spec, self.z, self.x, self.y, self.labels = ( - produce_Q_section_spectrum( - modes, - q_array, - h_array, - k_array, - ecenter=self.parameters_qplanes.ecenter, - deltaE=self.parameters_qplanes.deltaE, - bins=self.parameters_qplanes.bins, - spectrum_type=self.parameters_qplanes.spectrum_type, - dw=dw, - labels=labels, - ) + self.av_spec, self.z, self.x, self.y, self.labels = produce_Q_section_spectrum( + modes, + q_array, + h_array, + k_array, + ecenter=self.parameters_qplanes.ecenter, + deltaE=self.parameters_qplanes.deltaE, + bins=self.parameters_qplanes.bins, + spectrum_type=self.parameters_qplanes.spectrum_type, + dw=dw, + labels=labels, ) self.xlabel = "AAA" - self.ylabel = "AAA" - + self.ylabel = "AAA" + def _curate_path_and_labels( self, ): @@ -269,16 +286,33 @@ def _curate_path_and_labels( 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( + phonopy_yaml, fc_hdf5 = generate_force_constant_from_phonopy( self.node.phonon_bands.creator, mode="download" ) return phonopy_yaml, fc_hdf5 - + def prepare_data_for_download(self): raise NotImplementedError("Need to implement the download of a CSV file") - + # return data, filename + + @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) + def _clone(self): - # in case we want to clone the model. + # in case we want to clone the model. # This is the case when we have the same data and we inject in three # different models: we don't need to fetch three times. - return copy.deepcopy(self) \ No newline at end of file + return copy.deepcopy(self) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py index 8808fb3..a222f9f 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py @@ -1,19 +1,17 @@ -import pathlib -import tempfile import ipywidgets as ipw from IPython.display import display from aiidalab_qe.common.widgets import LoadingWidget -from aiidalab_qe_vibroscopy.utils.euphonic.data.structure_factors import ( - generate_force_constant_instance, +from aiidalab_qe_vibroscopy.app.widgets.structurefactorwidget import ( + EuphonicStructureFactorWidget, ) - -from aiidalab_qe_vibroscopy.app.widgets.structurefactorwidget import EuphonicStructureFactorWidget +from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import EuphonicResultsModel ##### START OVERALL WIDGET TO DISPLAY EVERYTHING: + class EuphonicWidget(ipw.VBox): """ Widget that will include everything, @@ -22,7 +20,11 @@ class EuphonicWidget(ipw.VBox): """ def __init__( - self, model: EuphonicResultsModel, node=None, detached_app = False, **kwargs, + self, + model: EuphonicResultsModel, + node=None, + detached_app=False, + **kwargs, ): """ Initialize the Euphonic utility class. @@ -50,11 +52,11 @@ def __init__( fc : optional Force constants if provided. """ - + super().__init__() - + self._model = model # this is the single crystal model. - if node: self._model.vibro = node + self._model.vibro = node self._model.detached_app = detached_app self._model.fc_hdf5_content = None @@ -63,7 +65,7 @@ def __init__( def render(self): if self.rendered: return - + self.tab_widget = ipw.Tab() self.tab_widget.layout.display = "none" self.tab_widget.set_title(0, "Single crystal") @@ -86,13 +88,18 @@ def render(self): if not self._model.detached_app: self.plot_button.disabled = False else: - from aiidalab_qe_vibroscopy.utils.euphonic.detached_app.uploadwidgets import UploadPhonopyWidget + from aiidalab_qe_vibroscopy.utils.euphonic.detached_app.uploadwidgets import ( + UploadPhonopyWidget, + ) + self.upload_widget = UploadPhonopyWidget() - self.upload_widget.reset_uploads.on_click(self._on_reset_uploads_button_clicked) + self.upload_widget.reset_uploads.on_click( + self._on_reset_uploads_button_clicked + ) 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.download_widget = DownloadYamlHdf5Widget(model=self._model) self.download_widget.layout.display = "none" @@ -101,8 +108,8 @@ def render(self): self.tab_widget, self.download_widget, self.loading_widget, - ) - + ) + self.rendered = True def _render_for_real(self, change=None): @@ -111,22 +118,23 @@ 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 = self._model._clone() - qsection_model = self._model._clone() + powder_model = EuphonicResultsModel(spectum_type="powder") + qsection_model = EuphonicResultsModel(spectum_type="q_planes") + + for data in ["fc", "q_path"]: + setattr(powder_model, data, getattr(self._model, data)) + setattr(qsection_model, data, getattr(self._model, data)) # 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() + singlecrystalwidget = EuphonicStructureFactorWidget( + node=self._model.vibro, model=self._model, spectrum_type="single_crystal" + ) self.tab_widget.children = ( singlecrystalwidget, - PowderFullWidget(model=powder_model), - QSectionFullWidget(model=qsection_model), + # 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: @@ -168,7 +176,8 @@ def _on_upload_hdf5(self, change): self._model.fc_hdf5_content = self.upload_widget.children[1].value[ fname ]["content"] - + + class DownloadYamlHdf5Widget(ipw.HBox): def __init__(self, model): self._model = model diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py index 51857fc..1fee1f1 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -1,21 +1,40 @@ +import ipywidgets as ipw +from IPython.display import display + +import numpy as np + # ADD ALL THE IMPORTS. +import plotly.graph_objs as go + +from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import EuphonicResultsModel + +COLORSCALE = "Viridis" +COLORBAR_DICT = dict(orientation="v", showticklabels=False, x=1, thickness=10, len=0.4) class EuphonicStructureFactorWidget(ipw.VBox): """Description. - + Collects all the button and widget used to define settings for Neutron dynamic structure factor, in all the three cases: single crystal, powder, and q-section.... """ - def __init__(self, model, node=None, spectrum_type = "single_crystal", detached_app = False, **kwargs): + def __init__( + self, + model: EuphonicResultsModel, + node=None, + spectrum_type="single_crystal", + detached_app=False, + **kwargs, + ): super().__init__() self._model = model - if node: self._model.vibro = node + if node: + self._model.vibro = node self._model.spectrum_type = spectrum_type self._model.detached_app = detached_app self.rendered = False - + def render(self): """Render the widget. @@ -23,9 +42,9 @@ def render(self): """ if self.rendered: return - + self._init_view() - + slider_intensity = ipw.FloatRangeSlider( value=[1, 100], # Default selected interval min=1, @@ -67,7 +86,7 @@ def render(self): ) ipw.link( (self._model, "q_spacing"), - (self.q_spacing, "value"), + (q_spacing, "value"), ) q_spacing.observe(self._on_setting_change, names="value") @@ -148,10 +167,32 @@ def render(self): layout=ipw.Layout(width="auto"), ) download_button.on_click(self._download_data) - + + self.children += ( + ipw.HBox( + [ + slider_intensity, + specification_intensity, + ], + layout=ipw.Layout( + justify_content="space-between", + margin="10px 0", + ), + ), + E_units_button, + q_spacing, + energy_broadening, + energy_bins, + temperature, + weight_button, + plot_button, + reset_button, + download_button, + ) + if self._model.spectrum_type == "single_crystal": self.custom_kpath_description = ipw.HTML( - """ + """
Custom q-points path for the structure factor:
we can provide it via a specific format:
@@ -176,18 +217,23 @@ def render(self): (self._model, "custom_kpath"), (self.custom_kpath_text, "value"), ) - self.custom_kpath_text.observe(self._on_setting_changed, names="value") + self.custom_kpath_text.observe(self._on_setting_change, names="value") + + self.children += ( + self.custom_kpath_description, + self.custom_kpath_text, + ) # fi self._model.spectrum_type == "single_crystal" elif self._model.spectrum_type == "powder": self.qmin = ipw.FloatText( - value=0, - description="|q|min (1/A)", + value=0, + description="|q|min (1/A)", ) ipw.link( (self._model, "q_min"), (self.qmin, "value"), ) - self.qmin.observe(self._on_setting_changed, names="value") + self.qmin.observe(self._on_setting_change, names="value") self.qmax = ipw.FloatText( step=0.01, @@ -198,7 +244,7 @@ def render(self): (self._model, "q_max"), (self.qmax, "value"), ) - self.qmax.observe(self._on_setting_changed, names="value") + self.qmax.observe(self._on_setting_change, names="value") self.int_npts = ipw.IntText( value=100, @@ -209,18 +255,18 @@ def render(self): (self._model, "npts"), (self.int_npts, "value"), ) - self.int_npts.observe(self._on_setting_changed, names="value") + self.int_npts.observe(self._on_setting_change, names="value") # fi self._model.spectrum_type == "powder" elif self._model.spectrum_type == "q_planes": self.ecenter = ipw.FloatText( - value=0, - description="E (meV)", + value=0, + description="E (meV)", ) ipw.link( (self._model, "center_e"), (self.ecenter, "value"), ) - self.ecenter.observe(self._on_setting_changed, names="value") + self.ecenter.observe(self._on_setting_change, names="value") self.plane_description_widget = ipw.HTML( """ @@ -266,7 +312,7 @@ def render(self): 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_setting_change, names="value") child.observe(self._on_vector_changed, names="value") self.Q0_widget = ipw.HBox( @@ -288,24 +334,24 @@ def render(self): (self._model, "energy_broadening"), (self.energy_broadening, "value"), ) - self.energy_broadening.observe(self._on_setting_changed, names="value") + self.energy_broadening.observe(self._on_setting_change, names="value") self.plot_button.disabled = False self.plot_button.description = "Plot" # self.reset_button.disabled = True self.download_button.disabled = True # fi self._model.spectrum_type == "q_planes" - - self.children += ( - ... - ) - + + self.children += (self.fig,) + self.rendered = True - + def _init_view(self, _=None): self._model.fetch_data() + if not hasattr(self, "fig"): + self.fig = go.FigureWidget() self._update_plot() - + def _on_plot_button_change(self, change): self.download_button.disabled = not change["new"] @@ -318,34 +364,35 @@ def _on_setting_change( self, change ): # think if we want to do something more evident... self.plot_button.disabled = False - + def _update_plot(self): # 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. # so the update_spectra need some more logic, or we call another method. self._model.get_spectra() - + if not self.rendered: # First time we render, we set several layout settings. # Layout settings - self.fig["layout"]["xaxis"].update( - title=self._model.xlabel, - range=[min(self._model.x), max(self._model.x)], - ) + if self._model.x: + self.fig["layout"]["xaxis"].update( + title=self._model.xlabel, + range=[min(self._model.x), max(self._model.x)], + ) self.fig["layout"]["yaxis"].update( title=self._model.ylabel, range=[min(self._model.y), max(self._model.y)], ) - + 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), + 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) @@ -354,17 +401,18 @@ def _update_plot(self): # Update the layout to enable autoscaling self.fig.update_layout(autosize=True) - heatmap_trace = go.Heatmap( - z=self._model.z, - y=(self._model.y), - x=self._model.x, - colorbar=COLORBAR_DICT, - colorscale=COLORSCALE, # imported from euphonic_base_widgets - ) + z=self._model.z, + y=(self._model.y), + x=self._model.x, + colorbar=COLORBAR_DICT, + colorscale=COLORSCALE, # imported from euphonic_base_widgets + ) # change the path wants also a change in the labels - if "ticks_positions" in self._model and "ticks_labels" in self._model: + if hasattr(self._model, "ticks_positions") and hasattr( + self._model, "ticks_labels" + ): self.fig.update_layout( xaxis=dict( tickmode="array", @@ -380,30 +428,35 @@ def _update_plot(self): # Add heatmap trace to figure self.fig.add_trace(heatmap_trace) - self.fig.data = [self.fig.data[1]] + self.fig.data = [self.fig.data[-1]] + + def _update_intensity_filter(self, change): + # 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 + + 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.fig["layout"]["yaxis"].update(title=change["new"]) def _reset_settings(self, _): self._model.reset() - + def _download_data(self, _=None): data, filename = self._model.prepare_data_for_download() - self._download(data, filename) - - @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) + self._model._download(data, filename) def _on_vector_changed(self, change=None): """ diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py index b0daf63..8655a6e 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/base_widgets/euphonic_base_widgets.py @@ -3,7 +3,7 @@ import plotly.graph_objects as go # from ..euphonic.bands_pdos import * -from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import * # noqa: F403 +from aiidalab_qe_vibroscopy.utils.euphonic.data.structure_factors import * # noqa: F403 # sys and os used to prevent euphonic to print in the stdout. diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/export_vibronic_to_euphonic.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/export_vibronic_to_euphonic.py index 7254e93..c5a4f75 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/export_vibronic_to_euphonic.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/export_vibronic_to_euphonic.py @@ -1,6 +1,7 @@ -from aiida.orm import Dict -from aiida_phonopy.common.raw_parsers import get_force_constants_from_phonopy -from aiida_phonopy.workflows.phonon import generate_force_constant_instance +from aiidalab_qe_vibroscopy.utils.euphonic.data.phonopy_interface import ( + generate_force_constant_from_phonopy, +) + def export_euphonic_data(output_vibronic, fermi_energy=None): if "phonon_bands" not in output_vibronic: @@ -32,7 +33,7 @@ def export_euphonic_data(output_vibronic, fermi_energy=None): q_path = None phonopy_calc = output_set.creator - fc = generate_force_constant_instance(phonopy_calc) + fc = generate_force_constant_from_phonopy(phonopy_calc) # bands = compute_bands(fc) # pdos = compute_pdos(fc) return { diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/phonopy_interface.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/phonopy_interface.py index 048257f..b580d65 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/phonopy_interface.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/phonopy_interface.py @@ -3,9 +3,15 @@ import base64 from typing import Optional import euphonic -from euphonic.io.phonopy import write_force_constants_to_hdf5 +from phonopy.file_IO import write_force_constants_to_hdf5 -def generate_force_constant_instance( +from aiidalab_qe_vibroscopy.utils.euphonic.data.structure_factors import ( + blockPrint, + enablePrint, +) + + +def generate_force_constant_from_phonopy( phonopy_calc=None, path: str = None, summary_name: str = None, @@ -115,4 +121,4 @@ def generate_force_constant_instance( # print(filename) # print(dirpath) enablePrint() - return fc \ No newline at end of file + return fc diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py index 6634dd4..edfeb6e 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py @@ -1,8 +1,5 @@ from typing import List, Optional -import pathlib -import tempfile -import base64 import matplotlib.style import numpy as np @@ -42,7 +39,11 @@ from euphonic.styles import intensity_widget_style import euphonic.util -from phonopy.file_IO import write_force_constants_to_hdf5 + +from aiidalab_qe_vibroscopy.utils.euphonic.data.parameters import ( + parameters_single_crystal, + parameters_powder, +) # Dummy tqdm function if tqdm progress bars unavailable try: @@ -199,11 +200,9 @@ def produce_bands_weigthed_data( """ # args = get_args(get_parser(), params) if not params: - args = AttrDict(copy.deepcopy(parameters)) + args = AttrDict(copy.deepcopy(parameters_single_crystal)) else: - args = copy.deepcopy(parameters) - args.update(params) - args = AttrDict(args) + args = AttrDict(params) # redundancy with args... calc_modes_kwargs = _calc_modes_kwargs(args) @@ -356,7 +355,7 @@ def produce_bands_weigthed_data( ######################## -#parameters_powder = AttrDict(par_dict_powder) +# parameters_powder = AttrDict(par_dict_powder) def produce_powder_data( @@ -369,11 +368,10 @@ def produce_powder_data( """Read the description of the produce_bands_weigthed_data function for more details. """ - # args = get_args(get_parser(), params) if not params: args = AttrDict(copy.deepcopy(parameters_powder)) else: - args = AttrDict(copy.deepcopy(params)) + args = AttrDict(params) # redundancy with args calc_modes_kwargs = _calc_modes_kwargs(args) diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py index 2a3ad09..c2a086d 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py @@ -7,10 +7,7 @@ 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, ) @@ -21,6 +18,9 @@ QSectionFullWidget, ) +from aiidalab_qe_vibroscopy.utils.euphonic.data.phonopy_interface import ( + generate_force_constant_from_phonopy, +) from aiidalab_qe.common.widgets import LoadingWidget ###### START for detached app: @@ -81,7 +81,7 @@ def _read_phonopy_files(self, fname, phonopy_yaml_content, fc_hdf5_content=None) temp_hdf5_name = temp_file.name try: - fc = generate_force_constant_instance( + fc = generate_force_constant_from_phonopy( path=pathlib.Path(fname), summary_name=temp_yaml.name, fc_name=temp_hdf5_name, @@ -94,7 +94,7 @@ def _read_phonopy_files(self, fname, phonopy_yaml_content, fc_hdf5_content=None) temp_hdf5_name = None try: - fc = generate_force_constant_instance( + fc = generate_force_constant_from_phonopy( path=pathlib.Path(fname), summary_name=temp_yaml.name, # fc_name=temp_hdf5_name, diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_q_planes_widgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_q_planes_widgets.py index 6fa8a5f..eca0af8 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_q_planes_widgets.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/tab_widgets/euphonic_q_planes_widgets.py @@ -21,7 +21,7 @@ from monty.json import jsanitize import plotly.graph_objects as go -from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import ( +from aiidalab_qe_vibroscopy.utils.euphonic.data.structure_factors import ( blockPrint, enablePrint, ) @@ -101,7 +101,7 @@ def produce_Q_section_spectrum( dw=None, labels=None, ): - from aiidalab_qe_vibroscopy.utils.euphonic.data_manipulation.intensity_maps import ( + from aiidalab_qe_vibroscopy.utils.euphonic.data.structure_factors import ( blockPrint, enablePrint, ) From 0a10129582f01993aa1b29e96198ef8ae77c9813 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 9 Dec 2024 17:34:39 +0000 Subject: [PATCH 05/16] 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() From 83f3400cf7b8ff4379053dfe49025b31f6493837 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 9 Dec 2024 17:59:37 +0000 Subject: [PATCH 06/16] Fixed energy units. --- .../app/widgets/euphonicmodel.py | 10 ++- .../app/widgets/structurefactorwidget.py | 65 +++++-------------- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index bed3d80..13fccc5 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -87,6 +87,8 @@ def _get_default(self, trait): return [1, 1, 1, 100, 1] elif trait == "Q0_vec": return [0.0, 0.0, 0.0] + elif trait == "intensity_filter": + return [0, 100] return self.traits()[trait].default_value def get_model_state(self): @@ -97,7 +99,8 @@ def reset( ): with self.hold_trait_notifications(): for trait in self.traits(): - setattr(self, trait, self._get_default(trait)) + if trait not in ["intensity_filter", "energy_units"]: + setattr(self, trait, self._get_default(trait)) def fetch_data(self): """Fetch the data from the database or from the uploaded files.""" @@ -290,6 +293,11 @@ def energy_conversion_factor(self, new, old): elif old == "THz": return self.THz_to_cm1 + def _update_energy_units(self, new, old): + # This is used to update the energy units in the plot. + self.y = self.y * self.energy_conversion_factor(new, old) + self.ylabel = f"Energy ({new})" + def _curate_path_and_labels( self, ): diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py index 7759911..dc60ebf 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -43,8 +43,6 @@ def render(self): if self.rendered: return - self._init_view() - slider_intensity = ipw.FloatRangeSlider( value=[1, 100], # Default selected interval min=1, @@ -71,7 +69,7 @@ def render(self): options=[ ("meV", "meV"), ("THz", "THz"), - # ("cm-1", "cm-1"), + ("cm-1", "cm-1"), ], value="meV", description="Energy units:", @@ -356,11 +354,6 @@ def render(self): ) self.energy_broadening.observe(self._on_setting_change, names="value") - self.plot_button.disabled = False - self.plot_button.description = "Plot" - # self.reset_button.disabled = True - self.download_button.disabled = True - self.children += ( self.ecenter, self.plane_description_widget, @@ -371,6 +364,7 @@ def render(self): ) # fi self._model.spectrum_type == "q_planes" + self._init_view() self.children += (self.figure_container,) self.rendered = True @@ -399,39 +393,11 @@ def _update_plot(self, _=None): # so the update_spectra need some more logic, or we call another method. 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 hasattr(self._model, "x"): - self.fig["layout"]["xaxis"].update( - title=self._model.xlabel, - range=[np.min(self._model.x), np.max(self._model.x)], - ) - self.fig["layout"]["yaxis"].update( - title=self._model.ylabel, - range=[np.min(self._model.y), np.max(self._model.y)], - ) + if self._model.spectrum_type == "q_planes" and not self.rendered: + # hide figure until we have the data + self.figure_container.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.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) - elif self._model.spectrum_type == "q_planes" and self.rendered: + if self._model.spectrum_type == "q_planes" and self.rendered: self.figure_container.layout.display = "block" heatmap_trace = go.Heatmap( @@ -466,7 +432,9 @@ def _update_plot(self, _=None): if self.rendered: self._update_intensity_filter() - def _update_intensity_filter(self): + self.plot_button.disabled = True + + def _update_intensity_filter(self, change=None): # the value of the intensity slider is in fractions of the max. # NOTE: we do this here, as we do not want to replot. self.fig.data[0].zmax = ( @@ -478,17 +446,14 @@ def _update_intensity_filter(self): def _update_energy_units(self, change): # the value of the intensity slider is in fractions of the max. - 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._model._update_energy_units( + new=change["new"], + old=change["old"], ) - - self.fig["layout"]["yaxis"].update(title=self._model.energy_units) - # Update x-axis and y-axis to enable autoscaling + + self.fig.data[0].y = self._model.y + self.fig.update_layout(yaxis_title=self._model.ylabel) self.fig.update_xaxes(autorange=True) self.fig.update_yaxes(autorange=True) From 3787eb03b883257e7005586711160fd569f7e0a9 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 9 Dec 2024 18:30:15 +0000 Subject: [PATCH 07/16] Energy units, also cm-1 --- .../app/widgets/euphonicmodel.py | 24 +++++++++---------- .../app/widgets/structurefactorwidget.py | 17 ++++++------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 13fccc5..761f355 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -66,7 +66,7 @@ def __init__(self, node=None, spectrum_type: str = "single_crystal", **kwargs): self.q_path = None self.spectrum_type = spectrum_type self.xlabel = None - self.ylabel = "Energy (meV)" + self.ylabel = self.energy_units self.detached_app = False if node: self.vibro = node @@ -201,13 +201,11 @@ def get_spectra( ) = generated_curated_data(spectra) self.z = final_zspectra.T - self.y = self.y[:, 0] * self.energy_conversion_factor( - self.energy_units, "meV" - ) + self.y = self.y[:, 0] # self.x = None # we have, instead, the ticks positions and labels self.xlabel = "" - self.ylabel = f"Energy ({self.energy_units})" + self.ylabel = self.energy_units elif self.spectrum_type == "powder": # powder case # Spectrum2D as output of the powder data @@ -218,13 +216,13 @@ 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 - self.y = self.y[:, 0] * self.energy_conversion_factor( - self.energy_units, "meV" - ) + self.y = self.y[:, 0] self.z = spectra.z_data.magnitude.T else: raise ValueError("Spectrum type not recognized:", self.spectrum_type) + self.y = self.y * self.energy_conversion_factor(self.energy_units, "meV") + def _get_qsection_spectra( self, ): @@ -280,14 +278,14 @@ def energy_conversion_factor(self, new, old): 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 old == "1/cm": + return 1 / self.THz_to_cm1 * self.THz_to_meV elif new == "THz": if old == "meV": return 1 / self.THz_to_meV - elif old == "cm-1": + elif old == "1/cm": return 1 / self.THz_to_cm1 - elif new == "cm-1": + elif new == "1/cm": if old == "meV": return 1 / self.THz_to_meV * self.THz_to_cm1 elif old == "THz": @@ -296,7 +294,7 @@ def energy_conversion_factor(self, new, old): def _update_energy_units(self, new, old): # This is used to update the energy units in the plot. self.y = self.y * self.energy_conversion_factor(new, old) - self.ylabel = f"Energy ({new})" + self.ylabel = self.energy_units def _curate_path_and_labels( self, diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py index dc60ebf..c04ec61 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -65,11 +65,11 @@ def render(self): "(Intensity is relative to the maximum intensity at T=0K)" ) - E_units_button = ipw.ToggleButtons( + E_units_ddown = ipw.Dropdown( options=[ ("meV", "meV"), ("THz", "THz"), - ("cm-1", "cm-1"), + ("1/cm", "1/cm"), ], value="meV", description="Energy units:", @@ -79,10 +79,10 @@ def render(self): ), ) ipw.link( - (E_units_button, "value"), + (E_units_ddown, "value"), (self._model, "energy_units"), ) - E_units_button.observe(self._update_energy_units, "value") + E_units_ddown.observe(self._update_energy_units, "value") # MAYBE WE LINK ALSO THIS TO THE MODEL? so we can download the data with the preferred units. q_spacing = ipw.FloatText( @@ -191,7 +191,7 @@ def render(self): margin="10px 0", ), ), - E_units_button, + E_units_ddown, q_spacing, energy_broadening, energy_bins, @@ -408,6 +408,8 @@ def _update_plot(self, _=None): colorscale=COLORSCALE, # imported from euphonic_base_widgets ) + self.fig.update_layout(yaxis_title=self._model.ylabel) + # change the path wants also a change in the labels if hasattr(self._model, "ticks_positions") and hasattr( self._model, "ticks_labels" @@ -454,11 +456,6 @@ def _update_energy_units(self, change): self.fig.data[0].y = self._model.y self.fig.update_layout(yaxis_title=self._model.ylabel) - 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 _reset_settings(self, _): self._model.reset() From 0b339d09a1b1f2ff907404e1338078f43705aa79 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 9 Dec 2024 19:15:43 +0000 Subject: [PATCH 08/16] Fixing single crystal and qplanes --- .../app/widgets/euphonicmodel.py | 63 ++++++++++--------- .../app/widgets/structurefactorwidget.py | 48 ++++++-------- 2 files changed, 53 insertions(+), 58 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 761f355..710c697 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -163,30 +163,28 @@ def get_spectra( if self.spectrum_type == "q_planes": self._get_qsection_spectra() - return - - self.parameters.update(self.get_model_state()) - - # 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": self.parameters["q_spacing"], - } else: - qpath = copy.deepcopy(self.q_path) - if qpath: - qpath["delta_q"] = self.parameters["q_spacing"] - - spectra, parameters = self._callback_spectra_generation( - params=AttrDict(self.parameters), - fc=self.fc, - linear_path=qpath, - plot=False, - ) + self.parameters.update(self.get_model_state()) + # 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": self.parameters["q_spacing"], + } + else: + qpath = copy.deepcopy(self.q_path) + if qpath: + qpath["delta_q"] = self.parameters["q_spacing"] + + spectra, parameters = self._callback_spectra_generation( + params=AttrDict(self.parameters), + fc=self.fc, + linear_path=qpath, + plot=False, + ) # curated spectra (labels and so on...) if self.spectrum_type == "single_crystal": # single crystal case @@ -196,15 +194,19 @@ def get_spectra( ( final_xspectra, final_zspectra, - self.ticks_positions, - self.ticks_labels, + ticks_positions, + ticks_labels, ) = generated_curated_data(spectra) + self.ticks_positions = ticks_positions + self.ticks_labels = ticks_labels + self.z = final_zspectra.T self.y = self.y[:, 0] - # self.x = None # we have, instead, the ticks positions and labels + self.x = list( + range(self.ticks_positions[-1]) + ) # we have, instead, the ticks positions and labels - self.xlabel = "" self.ylabel = self.energy_units elif self.spectrum_type == "powder": # powder case @@ -215,9 +217,12 @@ 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.xlabel = "|q| (1/A)" self.x = spectra.x_data.magnitude self.y = self.y[:, 0] self.z = spectra.z_data.magnitude.T + elif self.spectrum_type == "q_planes": + pass else: raise ValueError("Spectrum type not recognized:", self.spectrum_type) @@ -268,8 +273,8 @@ def _get_qsection_spectra( dw=dw, labels=labels, ) - self.xlabel = "AAA" - self.ylabel = "AAA" + self.xlabel = self.labels["h"] + self.ylabel = self.labels["k"] def energy_conversion_factor(self, new, old): # TODO: check this is correct. diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py index c04ec61..acdfce5 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -83,7 +83,6 @@ def render(self): (self._model, "energy_units"), ) E_units_ddown.observe(self._update_energy_units, "value") - # MAYBE WE LINK ALSO THIS TO THE MODEL? so we can download the data with the preferred units. q_spacing = ipw.FloatText( value=self._model.q_spacing, @@ -276,6 +275,9 @@ def render(self): # fi self._model.spectrum_type == "powder" elif self._model.spectrum_type == "q_planes": + E_units_ddown.layout.display = "none" + q_spacing.layout.display = "none" + self.ecenter = ipw.FloatText( value=0, description="E (meV)", @@ -343,24 +345,12 @@ def render(self): [ipw.HTML("k: ", layout={"width": "20px"}), self.k_vec] ) - self.energy_broadening = ipw.FloatText( - value=0.5, - description="ΔE (meV)", - tooltip="Energy window in eV", - ) - ipw.link( - (self._model, "energy_broadening"), - (self.energy_broadening, "value"), - ) - self.energy_broadening.observe(self._on_setting_change, names="value") - 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" @@ -388,25 +378,15 @@ def _on_setting_change( 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. - # so the update_spectra need some more logic, or we call another method. + self._model.get_spectra() - if self._model.spectrum_type == "q_planes" and not self.rendered: + if self._model.spectrum_type == "q_planes": # hide figure until we have the data - self.figure_container.layout.display = "none" - - if self._model.spectrum_type == "q_planes" and self.rendered: - self.figure_container.layout.display = "block" - - heatmap_trace = go.Heatmap( - z=self._model.z, - y=(self._model.y), - x=self._model.x, - colorbar=COLORBAR_DICT, - colorscale=COLORSCALE, # imported from euphonic_base_widgets - ) + self.figure_container.layout.display = ( + "none" if not self.rendered else "block" + ) + self.plot_button.disabled = self.rendered self.fig.update_layout(yaxis_title=self._model.ylabel) @@ -421,6 +401,16 @@ def _update_plot(self, _=None): ticktext=self._model.ticks_labels, ) ) + elif hasattr(self._model, "xlabel"): + self.fig.update_layout(xaxis_title=self._model.xlabel) + + heatmap_trace = go.Heatmap( + z=self._model.z, + y=self._model.y, + x=self._model.x, + colorbar=COLORBAR_DICT, + colorscale=COLORSCALE, + ) # Add colorbar colorbar = heatmap_trace.colorbar From aec54cfb53d6bf44b532170276f57968a41821c2 Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Wed, 8 Jan 2025 15:25:27 +0000 Subject: [PATCH 09/16] hiding the third tab INS, for now. --- src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py index b5d47c5..156608c 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py @@ -136,9 +136,9 @@ def _render_for_real(self, change=None): 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=qsection_model, spectrum_type="q_planes" + #), ) for widget in self.tab_widget.children: From 4b0b643fd090ae68ba44404531e5e08c5a5c8d64 Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Wed, 8 Jan 2025 15:40:48 +0000 Subject: [PATCH 10/16] explicitely "use_c" = False. --- .../app/widgets/euphonic/single_crystal_model.py | 2 +- src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py index 308aedb..8b03983 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py @@ -80,7 +80,7 @@ def update_parameters(self): "save_to": None, "asr": None, "dipole_parameter": 1.0, - "use_c": None, + "use_c": False, "n_threads": None, } diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py index 4db6364..d71f831 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py @@ -29,7 +29,7 @@ "save_to": None, "asr": None, # Apply an acoustic-sum-rule (ASR) correction to the data: "realspace" applies the correction to the force constant matrix in real space. "reciprocal" applies the correction to the dynamical matrix at each q-point. (default: None) "dipole_parameter": 1.0, # Set the cutoff in real/reciprocal space for the dipole Ewald sum; higher values use more reciprocal terms. If tuned correctly this can result in performance improvements. See euphonic-optimise-dipole-parameter program for help on choosing a good DIPOLE_PARAMETER. (default: 1.0) - "use_c": None, + "use_c": False, "n_threads": None, } From 06289c59a3b4f3f8fbf5c96a332f27b4f18a98fa Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 13 Jan 2025 10:29:12 +0000 Subject: [PATCH 11/16] Fixing the qplanes plots, and use_c restored the c extension is needed to speed up the calculation of the INS spectra. --- .../app/widgets/euphonic/single_crystal_model.py | 4 ++-- src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py | 4 ++-- src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py | 6 +++--- .../app/widgets/structurefactorwidget.py | 3 +-- .../utils/euphonic/data/parameters.py | 7 ++++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py index 8b03983..4727c43 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonic/single_crystal_model.py @@ -80,8 +80,8 @@ def update_parameters(self): "save_to": None, "asr": None, "dipole_parameter": 1.0, - "use_c": False, - "n_threads": None, + # "use_c": False, + # "n_threads": None, } def _update_spectra(self): diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 710c697..16199d1 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -48,7 +48,7 @@ class EuphonicResultsModel(Model): # Settings for single crystal and powder average q_spacing = tl.Float(0.01) - energy_broadening = tl.Float(0.05) + energy_broadening = tl.Float(0.5) energy_bins = tl.Int(200) temperature = tl.Float(0) weighting = tl.Unicode("coherent") @@ -261,7 +261,7 @@ def _get_qsection_spectra( temperature=self.parameters_qplanes.temperature, ) - self.av_spec, self.z, self.x, self.y, self.labels = produce_Q_section_spectrum( + self.z, q_array, self.x, self.y, self.labels = produce_Q_section_spectrum( modes, q_array, h_array, diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py index 156608c..b5d47c5 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py @@ -136,9 +136,9 @@ def _render_for_real(self, change=None): 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=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 acdfce5..05262c5 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -390,7 +390,7 @@ def _update_plot(self, _=None): self.fig.update_layout(yaxis_title=self._model.ylabel) - # change the path wants also a change in the labels + # changing the path wants also a change in the labels if hasattr(self._model, "ticks_positions") and hasattr( self._model, "ticks_labels" ): @@ -411,7 +411,6 @@ def _update_plot(self, _=None): colorbar=COLORBAR_DICT, colorscale=COLORSCALE, ) - # Add colorbar colorbar = heatmap_trace.colorbar colorbar.x = 1.05 # Move colorbar to the right diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py index d71f831..97c2452 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py @@ -1,9 +1,10 @@ """Set of parameters for given Euphonic calculation. -We distinguish between parameters for a single crystal and for a powder calculation: the former requires a path in reciprocal space, +We distinguish between parameters for a single crystal and for a powder calculation: the former requires a path in reciprocal space, while the latter requires a range of q-points. We have a set of common parameters that are shared between the two types of calculations. """ + common_parameters = { "weighting": "coherent", # Spectral weighting to plot: DOS, coherent inelastic neutron scattering (default: dos) "grid": None, # FWHM of broadening on q axis in 1/LENGTH_UNIT (no broadening if unspecified). (default: None) @@ -29,8 +30,8 @@ "save_to": None, "asr": None, # Apply an acoustic-sum-rule (ASR) correction to the data: "realspace" applies the correction to the force constant matrix in real space. "reciprocal" applies the correction to the dynamical matrix at each q-point. (default: None) "dipole_parameter": 1.0, # Set the cutoff in real/reciprocal space for the dipole Ewald sum; higher values use more reciprocal terms. If tuned correctly this can result in performance improvements. See euphonic-optimise-dipole-parameter program for help on choosing a good DIPOLE_PARAMETER. (default: 1.0) - "use_c": False, - "n_threads": None, + "use_c": True, + "n_threads": 1, } parameters_single_crystal = { From c63e72a4d872ec874f3a98b5474f64c8ad929a38 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 13 Jan 2025 15:21:14 +0000 Subject: [PATCH 12/16] Download of phonopy yaml and hdf5 data. --- .../app/widgets/euphonicmodel.py | 2 +- .../app/widgets/structurefactorwidget.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 16199d1..6e4e174 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -327,7 +327,7 @@ 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_from_phonopy( - self.node.phonon_bands.creator, mode="download" + self.vibro.phonon_bands.creator, mode="download" ) return phonopy_yaml, fc_hdf5 diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py index 05262c5..5db17f2 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -8,7 +8,7 @@ from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import EuphonicResultsModel -COLORSCALE = "Viridis" +COLORSCALE = "Viridis" # we should allow more options COLORBAR_DICT = dict(orientation="v", showticklabels=False, x=1, thickness=10, len=0.4) @@ -196,9 +196,14 @@ def render(self): energy_bins, self.temperature, weight_button, - self.plot_button, - reset_button, - self.download_button, + ipw.VBox( + [ + self.plot_button, + reset_button, + self.download_button, + ], + layout=ipw.Layout(justify_content="flex-start", max_width="200px"), + ), ) if self._model.spectrum_type == "single_crystal": From c9f7ef48e6c7f834c80741ece2917a5348ee6745 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 13 Jan 2025 16:08:09 +0000 Subject: [PATCH 13/16] Fixing the layout --- .../app/widgets/euphonicmodel.py | 2 +- .../app/widgets/structurefactorwidget.py | 98 ++++++++++++------- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 6e4e174..557eec7 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -140,7 +140,7 @@ def _inject_powder_settings( # 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)) + self.add_traits(npts=tl.Int(500)) def _inject_qsection_settings( self, diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py index 5db17f2..b139517 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -62,7 +62,7 @@ def render(self): slider_intensity.observe(self._update_intensity_filter, "value") specification_intensity = ipw.HTML( - "(Intensity is relative to the maximum intensity at T=0K)" + "Intensity window (relative to the maximum intensity at T=0K):" ) E_units_ddown = ipw.Dropdown( @@ -89,6 +89,9 @@ def render(self): step=0.001, description="q step (1/A)", tooltip="q spacing in 1/A", + layout=ipw.Layout( + width="auto", + ), ) ipw.link( (self._model, "q_spacing"), @@ -101,6 +104,9 @@ def render(self): step=0.01, description="ΔE (meV)", tooltip="Energy broadening in meV", + layout=ipw.Layout( + width="auto", + ), ) ipw.link( (self._model, "energy_broadening"), @@ -112,6 +118,9 @@ def render(self): value=self._model.energy_bins, description="#E bins", tooltip="Number of energy bins", + layout=ipw.Layout( + width="auto", + ), ) ipw.link( (self._model, "energy_bins"), @@ -124,6 +133,9 @@ def render(self): step=0.01, description="T (K)", disabled=False, + layout=ipw.Layout( + width="auto", + ), ) ipw.link( (self._model, "temperature"), @@ -140,6 +152,9 @@ def render(self): description="weight:", disabled=False, style={"description_width": "initial"}, + layout=ipw.Layout( + width="auto", + ), ) ipw.link( (self._model, "weighting"), @@ -179,30 +194,39 @@ def render(self): lambda x: not x, ) + self._init_view() # generate the self.figure_container + self.children += ( ipw.HBox( [ - slider_intensity, specification_intensity, + slider_intensity, ], layout=ipw.Layout( - justify_content="space-between", - margin="10px 0", + justify_content="flex-start", + # margin="10px 0", ), ), - E_units_ddown, - q_spacing, - energy_broadening, - energy_bins, - self.temperature, - weight_button, - ipw.VBox( + ipw.HBox( [ - self.plot_button, - reset_button, - self.download_button, + ipw.VBox( + [ + E_units_ddown, + q_spacing, + energy_broadening, + energy_bins, + self.temperature, + weight_button, + self.plot_button, + reset_button, + self.download_button, + ], + layout=ipw.Layout( + justify_content="flex-start", max_width="200px" + ), + ), + self.figure_container, ], - layout=ipw.Layout(justify_content="flex-start", max_width="200px"), ), ) @@ -262,20 +286,25 @@ def render(self): ) self.qmax.observe(self._on_setting_change, 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_change, names="value") + # int_npts fixed to 500 for now. + # 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_change, names="value") self.children += ( - self.qmin, - self.qmax, - self.int_npts, + ipw.HBox( + [ + self.qmin, + self.qmax, + ], + ), + # self.int_npts, ) # fi self._model.spectrum_type == "powder" @@ -359,15 +388,13 @@ def render(self): ) # fi self._model.spectrum_type == "q_planes" - self._init_view() - self.children += (self.figure_container,) - self.rendered = True def _init_view(self, _=None): self._model.fetch_data() if not hasattr(self, "fig"): self.fig = go.FigureWidget() + self.fig.update_layout(margin=dict(l=20, r=0, t=0, b=20)) self.figure_container = ipw.VBox([self.fig]) self._update_plot() @@ -413,13 +440,14 @@ def _update_plot(self, _=None): z=self._model.z, y=self._model.y, x=self._model.x, - colorbar=COLORBAR_DICT, + # colorbar=COLORBAR_DICT, colorscale=COLORSCALE, ) # Add colorbar - colorbar = heatmap_trace.colorbar - colorbar.x = 1.05 # Move colorbar to the right - colorbar.y = 0.5 # Center colorbar vertically + # Do we need it? + # colorbar = heatmap_trace.colorbar + # colorbar.x = 1.05 # Move colorbar to the right + # colorbar.y = 0.5 # Center colorbar vertically # Add heatmap trace to figure self.fig.add_trace(heatmap_trace) From 464244426f2b60619026b5d093bd2168a3169062 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Mon, 13 Jan 2025 16:45:50 +0000 Subject: [PATCH 14/16] fixing detached app and upload of files for INS. --- .../app/widgets/euphonicmodel.py | 18 ++++++++++---- .../app/widgets/euphonicwidget.py | 3 ++- .../euphonic/detached_app/Detached_app.ipynb | 24 ++++++++----------- .../euphonic/detached_app/uploadwidgets.py | 2 +- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 557eec7..08edbb2 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -58,7 +58,13 @@ class EuphonicResultsModel(Model): 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): + def __init__( + self, + node=None, + spectrum_type: str = "single_crystal", + detached_app: bool = False, + **kwargs, + ): super().__init__(**kwargs) self.spectra = {} @@ -67,8 +73,8 @@ def __init__(self, node=None, spectrum_type: str = "single_crystal", **kwargs): self.spectrum_type = spectrum_type self.xlabel = None self.ylabel = self.energy_units - self.detached_app = False - if node: + self.detached_app = detached_app + if node: # qe app mode. self.vibro = node if self.spectrum_type == "single_crystal": @@ -114,10 +120,12 @@ def fetch_data(self): self.q_path = ins_data["q_path"] # 2. from uploaded files... else: + # here we just use upload_widget as MVC all together, for simplicity. + # moreover, this part is not used in the current QE app. 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, + phonopy_yaml_content=self.phonopy_yaml_content, + fc_hdf5_content=self.fc_hdf5_content, ) def _inject_single_crystal_settings( diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py index b5d47c5..1355f0c 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicwidget.py @@ -98,6 +98,7 @@ def render(self): ) self.upload_widget.children[0].observe(self._on_upload_yaml, "_counter") self.upload_widget.children[1].observe(self._on_upload_hdf5, "_counter") + self._model.upload_widget = self.upload_widget self.children += (self.upload_widget,) self.download_widget = DownloadYamlHdf5Widget(model=self._model) @@ -166,7 +167,7 @@ def _on_upload_yaml(self, change): 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.fname = fname self._model.phonopy_yaml_content = self.upload_widget.children[0].value[ fname ]["content"] diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb index e66b899..5badf65 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb @@ -39,7 +39,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a2abe40204ac4a478c99cb6f8ba683b4", + "model_id": "152be27af20540b8aeeadd7fc2d2a462", "version_major": 2, "version_minor": 0 }, @@ -100,8 +100,8 @@ "from IPython.display import display\n", "from jinja2 import Environment\n", "\n", - "from aiidalab_qe_vibroscopy.utils.euphonic import static\n", - "from aiidalab_qe_vibroscopy.utils.euphonic import EuphonicSuperWidget" + "from aiidalab_qe_vibroscopy.utils.euphonic.detached_app import static\n", + "from aiidalab_qe_vibroscopy.app.widgets.euphonicwidget import EuphonicWidget" ] }, { @@ -113,7 +113,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "aec117638bca4fb998fcec2ce07f9d0d", + "model_id": "a4b4d2771766495f95056deffcc53a2b", "version_major": 2, "version_minor": 0 }, @@ -135,25 +135,21 @@ " '

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

'\n", ")\n", "\n", - "from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import EuphonicBaseResultsModel\n", + "from aiidalab_qe_vibroscopy.app.widgets.euphonicmodel import EuphonicResultsModel\n", "\n", - "widget = EuphonicSuperWidget(mode=\"detached\", model=EuphonicBaseResultsModel())\n", + "widget = EuphonicWidget(\n", + " model=EuphonicResultsModel(detached_app=True), detached_app=True\n", + ")\n", + "widget.render()\n", "\n", "output = ipw.Output()\n", "\n", "with output:\n", " display(welcome_message, widget, footer)\n", "\n", + "\n", "display(output)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "22387f54", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py index c2a086d..d573a74 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py @@ -221,7 +221,7 @@ def _on_upload_yaml(self, change): 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.fname = fname self._model.phonopy_yaml_content = self.upload_widget.children[0].value[ fname ]["content"] From 39bd9453fee0ffd8265d3d325e0a0f8d485a7967 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Thu, 16 Jan 2025 10:35:41 +0000 Subject: [PATCH 15/16] Adding plotting scripts for INS data. --- .../app/widgets/euphonicmodel.py | 65 +++++-- .../app/widgets/structurefactorwidget.py | 8 +- .../utils/euphonic/data/structure_factors.py | 66 +++---- .../euphonic/detached_app/Detached_app.ipynb | 4 +- .../euphonic/detached_app/uploadwidgets.py | 170 ------------------ .../utils/euphonic/plotting/generator.py | 27 +++ .../utils/euphonic/plotting/plot_heatmap.py | 38 ++++ .../euphonic/plotting/plot_heatmap.py.j2 | 53 ++++++ 8 files changed, 198 insertions(+), 233 deletions(-) create mode 100644 src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/generator.py create mode 100644 src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py create mode 100644 src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py.j2 diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 08edbb2..2cc75e4 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -47,13 +47,15 @@ class EuphonicResultsModel(Model): # 3. Q planes: qp # Settings for single crystal and powder average - q_spacing = tl.Float(0.01) - energy_broadening = tl.Float(0.5) - 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]) + q_spacing = tl.Float(0.1) # q-spacing for the linear path + energy_broadening = tl.Float(0.5) # energy broadening + energy_bins = tl.Int(200) # energy bins + temperature = tl.Float(0) # temperature + weighting = tl.Unicode("coherent") # weighting + energy_units = tl.Unicode("meV") # energy units + intensity_filter = tl.List( + trait=tl.Float(), default_value=[0, 100] + ) # intensity filter THz_to_meV = 4.13566553853599 # conversion factor. THz_to_cm1 = 33.3564095198155 # conversion factor. @@ -197,7 +199,7 @@ def get_spectra( # curated spectra (labels and so on...) if self.spectrum_type == "single_crystal": # single crystal case self.x, self.y = np.meshgrid( - spectra[0].x_data.magnitude, spectra[0].y_data.magnitude + spectra.x_data.magnitude, spectra.y_data.magnitude ) ( final_xspectra, @@ -212,9 +214,13 @@ def get_spectra( self.z = final_zspectra.T self.y = self.y[:, 0] self.x = list( - range(self.ticks_positions[-1]) + range(self.ticks_positions[-1] + 1) ) # we have, instead, the ticks positions and labels + # we need to cut out some of the x and y data, as they are not used in the plot. + self.y = self.y[: np.shape(self.z)[0]] + self.x = self.x[: np.shape(self.z)[1]] + self.ylabel = self.energy_units elif self.spectrum_type == "powder": # powder case @@ -229,6 +235,11 @@ def get_spectra( self.x = spectra.x_data.magnitude self.y = self.y[:, 0] self.z = spectra.z_data.magnitude.T + + # we need to cut out some of the x and y data, as they are not used in the plot. + self.y = self.y[: np.shape(self.z)[0]] + self.x = self.x[: np.shape(self.z)[1]] + elif self.spectrum_type == "q_planes": pass else: @@ -340,8 +351,38 @@ def produce_phonopy_files(self): return phonopy_yaml, fc_hdf5 def prepare_data_for_download(self): - raise NotImplementedError("Need to implement the download of a CSV file") - # return data, filename + import pandas as pd + import base64 + from aiidalab_qe_vibroscopy.utils.euphonic.plotting.generator import ( + generate_from_template, + ) + + random_number = np.random.randint(0, 100) + + # Plotted_data + df = pd.DataFrame(self.z, index=self.y, columns=self.x) + data = base64.b64encode(df.to_csv().encode()).decode() + filename = f"INS_structure_factor_{random_number}.csv" + + # model_state for template jinja plot script + model_state = self.get_model_state() + model_state["ylabel"] = self.ylabel + model_state["spectrum_type"] = self.spectrum_type + if hasattr(self, "ticks_labels"): + model_state["ticks_positions"] = self.ticks_positions + model_state["ticks_labels"] = self.ticks_labels + model_state["filename"] = filename + model_state["cmap"] = "cividis" + + plotting_script = generate_from_template(model_state) + plotting_script_data = base64.b64encode(plotting_script.encode()).decode() + plotting_script_filename = f"plot_script_{random_number}.py" + return [(data, filename), (plotting_script_data, plotting_script_filename)] + + def _download_data(self, _=None): + packed_data = self.prepare_data_for_download() + for data, filename in packed_data: + self._download(data, filename) @staticmethod def _download(payload, filename): @@ -350,7 +391,7 @@ def _download(payload, filename): javas = Javascript( """ var link = document.createElement('a'); - link.href = 'data:text/json;charset=utf-8;base64,{payload}' + link.href = 'data:text;charset=utf-8;base64,{payload}' link.download = "{filename}" document.body.appendChild(link); link.click(); diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py index b139517..c5feb45 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/structurefactorwidget.py @@ -181,13 +181,13 @@ def render(self): reset_button.on_click(self._reset_settings) self.download_button = ipw.Button( - description="Download Data and Plot", + description="Download Data", icon="download", button_style="primary", disabled=False, # Large files... layout=ipw.Layout(width="auto"), ) - self.download_button.on_click(self._download_data) + self.download_button.on_click(self._model._download_data) ipw.dlink( (self.plot_button, "disabled"), (self.download_button, "disabled"), @@ -482,10 +482,6 @@ def _update_energy_units(self, change): def _reset_settings(self, _): self._model.reset() - def _download_data(self, _=None): - data, filename = self._model.prepare_data_for_download() - self._model._download(data, filename) - def _on_vector_changed(self, change=None): """ Update the model. Specific to qplanes case. diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py index edfeb6e..0012cef 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py @@ -266,7 +266,6 @@ def produce_bands_weigthed_data( ) else: modes = data - split_args = {"btol": args.btol} x_tick_labels = get_qpoint_labels( modes.qpts, cell=modes.crystal.to_spglib_cell() ) @@ -328,9 +327,9 @@ def produce_bands_weigthed_data( if x_tick_labels: spectrum.x_tick_labels = x_tick_labels - spectra = spectrum.split(**split_args) # type: List[Spectrum2D] - if len(spectra) > 1: - pass # print(f"Found {len(spectra)} regions in q-point path") + spectra = spectrum # .split(**split_args) # type: List[Spectrum2D] + # if len(spectra) > 1: + # pass # print(f"Found {len(spectra)} regions in q-point path") if args.save_json: spectrum.to_json_file(args.save_json) @@ -584,44 +583,25 @@ def generated_curated_data(spectra): ticks_positions = [] ticks_labels = [] - final_xspectra = spectra[0].x_data.magnitude - final_zspectra = spectra[0].z_data.magnitude - for i in spectra[1:]: - final_xspectra = np.concatenate((final_xspectra, i.x_data.magnitude), axis=0) - final_zspectra = np.concatenate((final_zspectra, i.z_data.magnitude), axis=0) - - for j in spectra[:]: - # each spectra has the .x_tick_labels attribute, for the bands. - shift = False - for k in j.x_tick_labels: - ticks_positions.append(k[0]) - # ticks_labels.append("Gamma") if k[1] == '$\\Gamma$' else ticks_labels.append(k[1]) - ticks_labels.append(k[1]) - - # Here below we check if we are starting a new group, - # i.e. if the xticks count is starting again from 0 - # I also need to shift correctly the next index, which - # refers to the zero of the ticks_positions[-1]. - if len(ticks_positions) > 1: - if ticks_positions[-1] < ticks_positions[-2] or shift: - if ticks_positions[-1] == 0: # new linear path - ticks_positions.pop() - last = ticks_labels.pop().strip() - - # if the same index, do not join, just write once - if ticks_labels[-1].strip() != last: - ticks_labels[-1] = ticks_labels[-1].strip() + "|" + last - # the shift is needed because if this index was zero, - # the next one has to be shifted because it means that - # the index counting was restarted from zero, - # i.e. this is a new linear path. - - shift = True - else: - ticks_positions[-1] = ticks_positions[-1] + ticks_positions[-2] - - if ticks_labels[-1] == ticks_labels[-2]: - ticks_positions.pop() - ticks_labels.pop() + final_xspectra = spectra.x_data.magnitude + final_zspectra = spectra.z_data.magnitude + + for k in spectra.x_tick_labels: + ticks_positions.append(k[0]) + # ticks_labels.append("Gamma") if k[1] == '$\\Gamma$' else ticks_labels.append(k[1]) + ticks_labels.append(k[1]) + + if len(ticks_positions) > 1: + if ( + ticks_positions[-1] == ticks_positions[-2] + 1 + and ticks_labels[-1] != ticks_labels[-2] + ): + ticks_labels[-2] = ticks_labels[-2] + "|" + ticks_labels[-1] + ticks_positions.pop() + ticks_labels.pop() + + if ticks_labels[-1] == ticks_labels[-2]: + ticks_positions.pop() + ticks_labels.pop() return final_xspectra, final_zspectra, ticks_positions, ticks_labels diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb index 5badf65..e82c5f2 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/Detached_app.ipynb @@ -39,7 +39,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "152be27af20540b8aeeadd7fc2d2a462", + "model_id": "451e9bed65e14635b3725af244c2fbcd", "version_major": 2, "version_minor": 0 }, @@ -113,7 +113,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a4b4d2771766495f95056deffcc53a2b", + "model_id": "a92faf36ebfb4e2e93635377806933de", "version_major": 2, "version_minor": 0 }, diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py index d573a74..adaa38d 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/detached_app/uploadwidgets.py @@ -8,21 +8,11 @@ # from ..euphonic.bands_pdos import * -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_vibroscopy.utils.euphonic.data.phonopy_interface import ( generate_force_constant_from_phonopy, ) -from aiidalab_qe.common.widgets import LoadingWidget ###### START for detached app: @@ -108,166 +98,6 @@ def _read_phonopy_files(self, fname, phonopy_yaml_content, fc_hdf5_content=None) #### 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, - ] - - self.rendered = True - - 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._model.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 diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/generator.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/generator.py new file mode 100644 index 0000000..00152b6 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/generator.py @@ -0,0 +1,27 @@ +from jinja2 import Environment, FileSystemLoader +import datetime +import os + + +def generate_from_template(model_state): + # Load the Jinja environment and template + env = Environment(loader=FileSystemLoader(os.path.dirname(__file__))) + template = env.get_template("plot_heatmap.py.j2") + + # Define the context for the template + context = { + "generation_date": datetime.datetime.now().strftime("%Y-%m-%d"), + "model_state": model_state, + } + + # Render the template with the context + output = template.render(context) + + return output + + +if __name__ == "__main__": + # just for testing, we provide and empty metadata dictionary + settings = {} + script = generate_from_template(model_state=settings) + print(script) diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py new file mode 100644 index 0000000..2de9fb9 --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py @@ -0,0 +1,38 @@ +# to be able to plot: +# pip install pandas json matplotlib + + +import pandas as pd +import json +import matplotlib.pyplot as plt + +# Load the heatmap data from a CSV file +csv_file = "INS_structure_factor_22.csv" # Change this to your CSV file path +df = pd.read_csv(csv_file, index_col=0) + +json_file = "INS_metadata_22.json" # Change this to your JSON file path +with open(json_file, "r") as f: + metadata = json.load(f) + +# Create the heatmap +plt.figure(figsize=(10, 8)) +plt.imshow(df.values, cmap="cividis", aspect="auto") + +plt.ylabel = metadata["ylabel"] + +# Add color bar +plt.colorbar() + +# Add x and y axis labels +if "ticks_labels" in metadata: + plt.xticks(metadata["ticks_positions"], metadata["ticks_labels"]) + +if "spectrum_type" in metadata: + plt.title(f"Inelastic neutron scattering data - {metadata['spectrum_type']}") +else: + plt.title("Inelastic neutron scattering data") + +plt.gca().invert_yaxis() +# Show the plot +plt.tight_layout() +plt.show() diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py.j2 b/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py.j2 new file mode 100644 index 0000000..277f3ce --- /dev/null +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py.j2 @@ -0,0 +1,53 @@ +try: + import pandas as pd + import json + import matplotlib.pyplot as plt +except: + raise ImportError("Please install pandas, json and matplotlib to run this script.") +import os + +# This script was generated on {{ generation_date }} by the aiidalab-qe-vibroscopy plugin + +## settings, including the plotting parameters and csv filename; +## you can modify some of these: labels, cmap, filename. The rest is just a summary of the +## parameters used to produce the data. +settings = {{ model_state }} + +## Load the heatmap data from a CSV file and plotting. +# File paths +csv_file = settings['filename'] # Path to your CSV file + +# Check if files exist +if not os.path.exists(csv_file): + raise FileNotFoundError(f"CSV file not found: {csv_file}") + +# Load the data +df = pd.read_csv(csv_file, index_col=0) + +# Create the heatmap +plt.figure(figsize=(10, 8)) +plt.imshow(df.values, cmap=settings['cmap'], aspect='auto') + +# Add y-axis label +if 'ylabel' in settings: + plt.ylabel(settings['ylabel']) + +# Add color bar +plt.colorbar() + +# Add x and y axis labels from settings +if 'ticks_positions' in settings and 'ticks_labels' in settings: + plt.xticks(settings['ticks_positions'], settings['ticks_labels']) + +# Add title +if 'spectrum_type' in settings: + plt.title(f"Inelastic neutron scattering data - {settings['spectrum_type']}") +else: + plt.title("Inelastic neutron scattering data") + +# Invert the y-axis +plt.gca().invert_yaxis() + +# Show the plot +plt.tight_layout() +plt.show() From fe7da766144cdf0117d3969cb96f6cd24ee4f0f7 Mon Sep 17 00:00:00 2001 From: mikibonacci Date: Thu, 16 Jan 2025 13:07:37 +0000 Subject: [PATCH 16/16] Fixed the INS q_planes plots and download --- .../app/widgets/euphonicmodel.py | 24 ++-- .../utils/euphonic/data/structure_factors.py | 105 ++++++++++++++++++ .../euphonic/plotting/plot_heatmap.py.j2 | 23 +++- 3 files changed, 140 insertions(+), 12 deletions(-) diff --git a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py index 2cc75e4..67bc4b9 100644 --- a/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py +++ b/src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py @@ -8,6 +8,8 @@ produce_bands_weigthed_data, produce_powder_data, generated_curated_data, + produce_Q_section_spectrum, + produce_Q_section_modes, ) from aiidalab_qe_vibroscopy.utils.euphonic.data.phonopy_interface import ( @@ -24,12 +26,6 @@ ) -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 @@ -360,13 +356,27 @@ def prepare_data_for_download(self): random_number = np.random.randint(0, 100) # Plotted_data - df = pd.DataFrame(self.z, index=self.y, columns=self.x) + if self.spectrum_type == "q_planes": + # we store x,y,z as values, not as values, indexes and columns + z = self.z.reshape(int(self.k_vec[-2]) + 1, int(self.h_vec[-2]) + 1) + df = pd.DataFrame( + z, + index=self.y[: int(self.k_vec[-2]) + 1], + columns=self.x[:: int(self.h_vec[-2]) + 1], + ) + else: + df = pd.DataFrame(self.z, index=self.y, columns=self.x) + data = base64.b64encode(df.to_csv().encode()).decode() filename = f"INS_structure_factor_{random_number}.csv" # model_state for template jinja plot script model_state = self.get_model_state() model_state["ylabel"] = self.ylabel + if hasattr(self, "xlabel"): + model_state["xlabel"] = self.xlabel + if hasattr(self, "Q0_vec"): + model_state["Q0"] = self.Q0_vec model_state["spectrum_type"] = self.spectrum_type if hasattr(self, "ticks_labels"): model_state["ticks_positions"] = self.ticks_positions diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py index 0012cef..444ede2 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/data/structure_factors.py @@ -577,6 +577,111 @@ def update_max(max_val): matplotlib_save_or_show(save_filename=args.save_to) +def produce_Q_section_modes( + fc, + h, + k, + Q0=np.array([0, 0, 0]), + n_h=100, + n_k=100, + h_extension=1, + k_extension=1, + temperature=0, +): + from euphonic import ureg + + # see get_Q_section + # h: array vector + # k: array vector + # Q0: "point" in Q-space used to build the portion of plane, using also the two vectors h and k. + # n_h, n_k: number of points along the two directions. or better, the two vectors. + + def get_Q_section(h, k, Q0, n_h, n_k, h_extension, k_extension): + # every point in the space is Q=Q0+dv1*h+dv2*k, which + + q_list = [] + h_list = [] + k_list = [] + + for dv1 in np.linspace(-h_extension, h_extension, n_h): + for dv2 in np.linspace(-k_extension, k_extension, n_k): + p = Q0 + dv1 * h + dv2 * k + q_list.append(p) # Q list + h_list.append(dv1) # *h[0]) + k_list.append(dv2) # *k[1]) + + return np.array(q_list), np.array(h_list), np.array(k_list) + + q_array, h_array, k_array = get_Q_section( + h, k, Q0, n_h + 1, n_k + 1, h_extension, k_extension + ) + + modes = fc.calculate_qpoint_phonon_modes(qpts=q_array, reduce_qpts=False) + + if temperature > 0: + blockPrint() + dw = _get_debye_waller( + temperature * ureg("K"), + fc, + # grid_spacing=(args.grid_spacing * recip_length_unit), + # **calc_modes_kwargs, + ) + enablePrint() + else: + dw = None + + labels = { + "q": f"Q0={[np.round(i,3) for i in Q0]}", + "h": f"h={[np.round(i,3) for i in h]}", + "k": f"k={[np.round(i,3) for i in k]}", + } + + return modes, q_array, h_array, k_array, labels, dw + + +def produce_Q_section_spectrum( + modes, + q_array, + h_array, + k_array, + ecenter, + deltaE=0.5, + bins=10, + spectrum_type="coherent", + dw=None, + labels=None, +): + from aiidalab_qe_vibroscopy.utils.euphonic.data.structure_factors import ( + blockPrint, + enablePrint, + ) + + # bins = 10 # hard coded beacuse here it does not change anything. + ebins = _get_energy_bins( + modes, bins + 1, emin=ecenter - deltaE, emax=ecenter + deltaE + ) + + blockPrint() + if ( + spectrum_type == "coherent" + ): # Temperature?? For now let's drop it otherwise it is complicated. + spectrum = modes.calculate_structure_factor(dw=dw).calculate_sqw_map(ebins) + elif spectrum_type == "dos": + spectrum = modes.calculate_dos_map(ebins) + + mu = ecenter + sigma = (deltaE) / 2 + + # Gaussian weights. + weights = np.exp(-((spectrum.y_data.magnitude - mu) ** 2) / 2 * sigma**2) / np.sqrt( + 2 * np.pi * sigma**2 + ) + av_spec = np.average(spectrum.z_data.magnitude, axis=1, weights=weights[:-1]) + enablePrint() + + return av_spec, q_array, h_array, k_array, labels + + def generated_curated_data(spectra): # here we concatenate the bands groups and create the ticks and labels. diff --git a/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py.j2 b/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py.j2 index 277f3ce..4feea47 100644 --- a/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py.j2 +++ b/src/aiidalab_qe_vibroscopy/utils/euphonic/plotting/plot_heatmap.py.j2 @@ -2,6 +2,7 @@ try: import pandas as pd import json import matplotlib.pyplot as plt + from matplotlib.ticker import MaxNLocator except: raise ImportError("Please install pandas, json and matplotlib to run this script.") import os @@ -12,6 +13,7 @@ import os ## you can modify some of these: labels, cmap, filename. The rest is just a summary of the ## parameters used to produce the data. settings = {{ model_state }} +spectrum_type = settings.get('spectrum_type', 'single_crystal') ## Load the heatmap data from a CSV file and plotting. # File paths @@ -26,12 +28,23 @@ df = pd.read_csv(csv_file, index_col=0) # Create the heatmap plt.figure(figsize=(10, 8)) -plt.imshow(df.values, cmap=settings['cmap'], aspect='auto') + +plt.pcolormesh(df.columns, df.index, df.values, cmap=settings['cmap'], shading='auto') + +# Limit the number of ticks in the x axis +## This is to avoid having too many ticks in the x-axis, which can make the plot unreadable +## NB: you can change it at you convenience +max_xticks = 9 +plt.gca().xaxis.set_major_locator(MaxNLocator(nbins=max_xticks)) # Add y-axis label if 'ylabel' in settings: plt.ylabel(settings['ylabel']) +# Add x-axis label +if 'xlabel' in settings: + plt.xlabel(settings['xlabel']) + # Add color bar plt.colorbar() @@ -40,13 +53,13 @@ if 'ticks_positions' in settings and 'ticks_labels' in settings: plt.xticks(settings['ticks_positions'], settings['ticks_labels']) # Add title -if 'spectrum_type' in settings: - plt.title(f"Inelastic neutron scattering data - {settings['spectrum_type']}") +if "Q0" in settings: + plt.title(f"Inelastic neutron scattering data - {spectrum_type} - Q0 = {settings['Q0']}") else: - plt.title("Inelastic neutron scattering data") + plt.title(f"Inelastic neutron scattering data - {spectrum_type}") # Invert the y-axis -plt.gca().invert_yaxis() +if spectrum_type != "q_planes": plt.gca().invert_yaxis() # Show the plot plt.tight_layout()