Skip to content

Commit

Permalink
Refactoring Euphonic MVC.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mikibonacci committed Dec 8, 2024
1 parent fbb0075 commit f256211
Show file tree
Hide file tree
Showing 7 changed files with 759 additions and 287 deletions.
143 changes: 83 additions & 60 deletions src/aiidalab_qe_vibroscopy/app/widgets/euphonicmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,42 @@ 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)
energy_broadening = tl.Float(0.05)
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."""
Expand All @@ -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))
Expand All @@ -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]]),
Expand All @@ -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.
Expand All @@ -240,13 +257,19 @@ 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
phonopy_yaml, fc_hdf5 = generate_force_constant_instance(
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)
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

########################
Expand Down
Original file line number Diff line number Diff line change
@@ -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}
52 changes: 52 additions & 0 deletions src/aiidalab_qe_vibroscopy/utils/euphonic/data/parameters.py
Original file line number Diff line number Diff line change
@@ -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,
}
Loading

0 comments on commit f256211

Please sign in to comment.