diff --git a/src/aiidalab_qe/app/configuration/advanced/advanced.py b/src/aiidalab_qe/app/configuration/advanced/advanced.py index 563c1377f..4a43bc40a 100644 --- a/src/aiidalab_qe/app/configuration/advanced/advanced.py +++ b/src/aiidalab_qe/app/configuration/advanced/advanced.py @@ -50,6 +50,11 @@ def __init__(self, model: AdvancedConfigurationSettingsModel, **kwargs): "kpoints_distance", ) + # NOTE connect pseudos first, as some settings depend on it + pseudos_model = PseudosConfigurationSettingsModel() + self.pseudos = PseudosConfigurationSettingsPanel(model=pseudos_model) + model.add_model("pseudos", pseudos_model) + smearing_model = SmearingConfigurationSettingsModel() self.smearing = SmearingConfigurationSettingsPanel(model=smearing_model) model.add_model("smearing", smearing_model) @@ -64,10 +69,6 @@ def __init__(self, model: AdvancedConfigurationSettingsModel, **kwargs): self.hubbard = HubbardConfigurationSettingsPanel(model=hubbard_model) model.add_model("hubbard", hubbard_model) - pseudos_model = PseudosConfigurationSettingsModel() - self.pseudos = PseudosConfigurationSettingsPanel(model=pseudos_model) - model.add_model("pseudos", pseudos_model) - def render(self): if self.rendered: return diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py index 7e4cd75d6..f9bd79904 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py @@ -8,10 +8,10 @@ class MagnetizationConfigurationSettingsPanel( AdvancedConfigurationSubSettingsPanel[MagnetizationConfigurationSettingsModel], ): """Widget to set the type of magnetization used in the calculation: - 1) Tot_magnetization: Total majority spin charge - minority spin charge. - 2) Starting magnetization: Starting spin polarization on atomic type 'i' in a spin polarized (LSDA or noncollinear/spin-orbit) calculation. + 1) Total magnetization: Total majority spin charge - minority spin charge. + 2) Magnetic moments: Starting spin polarization on atomic type 'i' in a spin polarized (LSDA or noncollinear/spin-orbit) calculation. - For Starting magnetization you can set each kind names defined in the StructureData (StructureData.get_kind_names()) + For Magnetic moments you can set each kind names defined in the StructureData (StructureData.get_kind_names()) Usually these are the names of the elements in the StructureData (For example 'C' , 'N' , 'Fe' . However the StructureData can have defined kinds like 'Fe1' and 'Fe2') The widget generate a dictionary that can be used to set initial_magnetic_moments in the builder of PwBaseWorkChain @@ -44,13 +44,20 @@ def render(self): if self.rendered: return - self.description = ipw.HTML("Magnetization:") + self.description = ipw.HTML(""" +
+ Magnetization: +
+ Default magnetic moments correspond to theoretical values. +
+ """) self.magnetization_type = ipw.ToggleButtons( style={ "description_width": "initial", "button_width": "initial", }, + layout=ipw.Layout(margin="0 0 10px 0"), ) ipw.dlink( (self._model, "type_options"), diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py index fafd44ff5..41c84d301 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py @@ -2,7 +2,10 @@ import traitlets as tl +from aiida.common.exceptions import NotExistent +from aiida_quantumespresso.workflows.protocols.utils import get_starting_magnetization from aiidalab_qe.common.mixins import HasInputStructure +from aiidalab_qe.utils import fetch_pseudo_family_by_label from ..subsettings import AdvancedCalculationSubSettingsModel @@ -17,15 +20,17 @@ class MagnetizationConfigurationSettingsModel( "input_structure", "electronic_type", "spin_type", + "pseudos.family", ] electronic_type = tl.Unicode() spin_type = tl.Unicode() + family = tl.Unicode() type_options = tl.List( trait=tl.List(tl.Unicode()), default_value=[ - ["Starting magnetization", "starting_magnetization"], + ["Magnetic moments", "starting_magnetization"], ["Total magnetization", "tot_magnetization"], ], ) @@ -41,9 +46,22 @@ def update(self, specific=""): # noqa: ARG002 if self.spin_type == "none" or not self.has_structure: self._defaults["moments"] = {} else: - self._defaults["moments"] = { - kind_name: 0.0 for kind_name in self.input_structure.get_kind_names() - } + try: + family = fetch_pseudo_family_by_label(self.family) + initial_guess = get_starting_magnetization(self.input_structure, family) + self._defaults["moments"] = { + kind.name: round( + initial_guess[kind.name] + * family.get_pseudo(kind.symbol).z_valence, + 3, + ) + for kind in self.input_structure.kinds + } + except NotExistent: + self._defaults["moments"] = { + kind_name: 0.0 + for kind_name in self.input_structure.get_kind_names() + } with self.hold_trait_notifications(): self.moments = self._get_default_moments() diff --git a/src/aiidalab_qe/app/configuration/advanced/model.py b/src/aiidalab_qe/app/configuration/advanced/model.py index 9bc75a63c..a09253701 100644 --- a/src/aiidalab_qe/app/configuration/advanced/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/model.py @@ -255,11 +255,7 @@ def _link_model(self, model: AdvancedCalculationSubSettingsModel): self._on_any_change, tl.All, ) - for trait in model.dependencies: - ipw.dlink( - (self, trait), - (model, trait), - ) + super()._link_model(model) def _update_kpoints_mesh(self, _=None): if not self.has_structure: diff --git a/src/aiidalab_qe/app/configuration/advanced/pseudos/model.py b/src/aiidalab_qe/app/configuration/advanced/pseudos/model.py index bf9231fd2..725b3c230 100644 --- a/src/aiidalab_qe/app/configuration/advanced/pseudos/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/pseudos/model.py @@ -5,13 +5,13 @@ import traitlets as tl from aiida_pseudo.common.units import U -from aiida import orm from aiida.common import exceptions from aiida.plugins import GroupFactory from aiida_quantumespresso.workflows.pw.base import PwBaseWorkChain from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS from aiidalab_qe.common.mixins import HasInputStructure from aiidalab_qe.setup.pseudos import PSEUDODOJO_VERSION, SSSP_VERSION, PseudoFamily +from aiidalab_qe.utils import fetch_pseudo_family_by_label from ..subsettings import AdvancedCalculationSubSettingsModel @@ -132,7 +132,7 @@ def update_default_pseudos(self): self.status_message = "" try: - pseudo_family = self._get_pseudo_family_from_database() + pseudo_family = fetch_pseudo_family_by_label(self.family) pseudos = pseudo_family.get_pseudos(structure=self.input_structure) except ValueError as exception: self.status_message = f""" @@ -156,7 +156,7 @@ def update_default_cutoffs(self): self.status_message = "" try: - pseudo_family = self._get_pseudo_family_from_database() + pseudo_family = fetch_pseudo_family_by_label(self.family) current_unit = pseudo_family.get_cutoffs_unit() cutoff_dict = pseudo_family.get_cutoffs() except exceptions.NotExistent: @@ -301,21 +301,6 @@ def _get_default(self, trait): ) return self._defaults.get(trait, self.traits()[trait].default_value) - def _get_pseudo_family_from_database(self): - """Get the pseudo family from the database.""" - return ( - orm.QueryBuilder() - .append( - ( - PseudoDojoFamily, - SsspFamily, - CutoffsPseudoPotentialFamily, - ), - filters={"label": self.family}, - ) - .one()[0] - ) - def _get_default_dictionary(self): return deepcopy(self._defaults["dictionary"]) diff --git a/src/aiidalab_qe/app/configuration/basic/basic.py b/src/aiidalab_qe/app/configuration/basic/basic.py index a92c88fd7..9d393d695 100644 --- a/src/aiidalab_qe/app/configuration/basic/basic.py +++ b/src/aiidalab_qe/app/configuration/basic/basic.py @@ -45,17 +45,18 @@ def render(self): (self._model, "spin_type"), (self.spin_type, "value"), ) + self.spin_type.observe( + self._on_spin_type_change, + "value", + ) + self.magnetization_info = ipw.HTML( value="""
Set the desired magnetic configuration in advanced settings
""", - layout=ipw.Layout(visibility="hidden"), - ) - self.spin_type.observe( - self._on_spin_type_change, - "value", + layout=ipw.Layout(display="none"), ) # Spin-Orbit calculation @@ -80,6 +81,29 @@ def render(self): (self.protocol, "value"), ) + self.warning = ipw.HTML( + value=""" +
+

+ Warning: detected multiples atoms with different tags. + You may be interested in an antiferromagnetic system. Note that + default starting magnetic moments do not distinguish tagged + atoms and are set to the same value. +

+

+ Please go to Advanced settings and override the default + values, specifying appropriate magnetic moments for each + species (e.g. with different signs for an antiferromagnetic + configuration). +

+
+ """, + layout=ipw.Layout(display="none"), + ) + self.children = [ InAppGuide(identifier="basic-settings"), ipw.HTML(""" @@ -153,6 +177,7 @@ def render(self): (at the price of longer/costlier calculations). """), + self.warning, ] self.rendered = True @@ -161,7 +186,10 @@ def _on_input_structure_change(self, _): self.refresh(specific="structure") def _on_spin_type_change(self, _): - if self.spin_type.value == "none": - self.magnetization_info.layout.visibility = "hidden" + if self._model.spin_type == "collinear": + self.magnetization_info.layout.display = "block" + if self._model.has_tags: + self.warning.layout.display = "flex" else: - self.magnetization_info.layout.visibility = "visible" + self.magnetization_info.layout.display = "none" + self.warning.layout.display = "none" diff --git a/src/aiidalab_qe/app/configuration/basic/model.py b/src/aiidalab_qe/app/configuration/basic/model.py index e9e826741..f526e547d 100644 --- a/src/aiidalab_qe/app/configuration/basic/model.py +++ b/src/aiidalab_qe/app/configuration/basic/model.py @@ -1,13 +1,16 @@ import traitlets as tl -from aiida import orm from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS +from aiidalab_qe.common.mixins import HasInputStructure from aiidalab_qe.common.panel import ConfigurationSettingsModel DEFAULT: dict = DEFAULT_PARAMETERS # type: ignore -class BasicConfigurationSettingsModel(ConfigurationSettingsModel): +class BasicConfigurationSettingsModel( + ConfigurationSettingsModel, + HasInputStructure, +): title = "Basic settings" identifier = "workchain" @@ -15,8 +18,6 @@ class BasicConfigurationSettingsModel(ConfigurationSettingsModel): "input_structure", ] - input_structure = tl.Union([tl.Instance(orm.StructureData)], allow_none=True) - protocol_options = tl.List( trait=tl.Tuple(tl.Unicode(), tl.Unicode()), default_value=[ diff --git a/src/aiidalab_qe/app/configuration/model.py b/src/aiidalab_qe/app/configuration/model.py index 36a9c3e84..ceb19dc07 100644 --- a/src/aiidalab_qe/app/configuration/model.py +++ b/src/aiidalab_qe/app/configuration/model.py @@ -129,18 +129,7 @@ def _link_model(self, model: ConfigurationSettingsModel): (self, "confirmed"), (model, "confirmed"), ) - for dependency in model.dependencies: - dependency_parts = dependency.split(".") - if len(dependency_parts) == 1: # from parent, e.g. input_structure - target_model = self - trait = dependency - else: # from sibling, e.g. workchain.protocol - sibling, trait = dependency_parts - target_model = self.get_model(sibling) - ipw.dlink( - (target_model, trait), - (model, trait), - ) + super()._link_model(model) def _get_properties(self): properties = [] diff --git a/src/aiidalab_qe/common/mixins.py b/src/aiidalab_qe/common/mixins.py index 7dbe5df80..e6629a3e4 100644 --- a/src/aiidalab_qe/common/mixins.py +++ b/src/aiidalab_qe/common/mixins.py @@ -26,6 +26,13 @@ def has_structure(self): def has_pbc(self): return not self.has_structure or any(self.input_structure.pbc) + @property + def has_tags(self): + return any( + not kind_name.isalpha() + for kind_name in self.input_structure.get_kind_names() + ) + class HasModels(t.Generic[T]): def __init__(self): @@ -51,7 +58,20 @@ def get_models(self) -> t.Iterable[tuple[str, T]]: return self._models.items() def _link_model(self, model: T): - pass + if not hasattr(model, "dependencies"): + return + for dependency in model.dependencies: + dependency_parts = dependency.split(".") + if len(dependency_parts) == 1: # from parent + target_model = self + trait = dependency + else: # from sibling + sibling, trait = dependency_parts + target_model = self.get_model(sibling) + tl.dlink( + (target_model, trait), + (model, trait), + ) class HasProcess(tl.HasTraits): diff --git a/src/aiidalab_qe/utils.py b/src/aiidalab_qe/utils.py index 17e8013f4..398782ca3 100644 --- a/src/aiidalab_qe/utils.py +++ b/src/aiidalab_qe/utils.py @@ -1,3 +1,5 @@ +from aiida_pseudo.groups.family import PseudoPotentialFamily + from aiida import orm @@ -30,3 +32,8 @@ def enable_pencil_decomposition(component): """Enable the pencil decomposition for the given component.""" component.settings = orm.Dict({"CMDLINE": ["-pd", ".true."]}) + + +def fetch_pseudo_family_by_label(label) -> PseudoPotentialFamily: + """Fetch the pseudo family by label.""" + return orm.Group.collection.get(label=label) # type: ignore