From b60b51d22e242525eac7782bf67aef2dd68393bd Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sat, 28 Dec 2024 06:26:55 +0000 Subject: [PATCH 01/13] Refactor pseudo family fetching as a utility --- .../configuration/advanced/pseudos/model.py | 21 +++---------------- src/aiidalab_qe/utils.py | 5 +++++ 2 files changed, 8 insertions(+), 18 deletions(-) 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/utils.py b/src/aiidalab_qe/utils.py index 17e8013f4..0bcc1dca3 100644 --- a/src/aiidalab_qe/utils.py +++ b/src/aiidalab_qe/utils.py @@ -30,3 +30,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): + """Fetch the pseudo family by label.""" + return orm.Group.collection.get(label=label) From d793417edaf8fd007542c758dc6042fba20d3f5e Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sat, 28 Dec 2024 06:28:03 +0000 Subject: [PATCH 02/13] Generalize model linking --- .../app/configuration/advanced/model.py | 6 +----- src/aiidalab_qe/app/configuration/model.py | 13 +------------ src/aiidalab_qe/common/mixins.py | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 18 deletions(-) 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/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..fa43a1632 100644 --- a/src/aiidalab_qe/common/mixins.py +++ b/src/aiidalab_qe/common/mixins.py @@ -51,7 +51,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): From 22467302c5b5266a0c4b1c54a2e893275b8e8f29 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sat, 28 Dec 2024 06:28:57 +0000 Subject: [PATCH 03/13] Use qe plugin utility to define default starting magnetization --- .../app/configuration/advanced/advanced.py | 9 +++++---- .../advanced/magnetization/magnetization.py | 10 +++++++++- .../advanced/magnetization/model.py | 18 +++++++++++++++--- 3 files changed, 29 insertions(+), 8 deletions(-) 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..99801e754 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py @@ -44,7 +44,15 @@ def render(self): if self.rendered: return - self.description = ipw.HTML("Magnetization:") + self.description = ipw.HTML(""" +
+ Magnetization: +
+ The default starting magnetization is computed as the theoretical + magnetic moment scaled by the valence charge defined in the selected + pseudopotential family. +
+ """) self.magnetization_type = ipw.ToggleButtons( style={ diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py index fafd44ff5..4d30c4b61 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,10 +20,12 @@ 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()), @@ -41,9 +46,16 @@ 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: + self._defaults["moments"] = get_starting_magnetization( + self.input_structure, + fetch_pseudo_family_by_label(self.family), + ) + 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() From 6abf801b335d7f5e08b9cb8c5c59756dc3de7090 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sat, 28 Dec 2024 07:28:12 +0000 Subject: [PATCH 04/13] Implement warning for tagged magnetic structures --- .../app/configuration/basic/basic.py | 37 ++++++++++++++++--- .../app/configuration/basic/model.py | 9 +++-- src/aiidalab_qe/common/mixins.py | 7 ++++ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/basic/basic.py b/src/aiidalab_qe/app/configuration/basic/basic.py index a92c88fd7..ca0e1588b 100644 --- a/src/aiidalab_qe/app/configuration/basic/basic.py +++ b/src/aiidalab_qe/app/configuration/basic/basic.py @@ -45,6 +45,11 @@ 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="""
@@ -53,10 +58,6 @@ def render(self): """, layout=ipw.Layout(visibility="hidden"), ) - self.spin_type.observe( - self._on_spin_type_change, - "value", - ) # Spin-Orbit calculation self.spin_orbit = ipw.ToggleButtons(style={"description_width": "initial"}) @@ -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 starting magnetization 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,9 @@ def _on_input_structure_change(self, _): self.refresh(specific="structure") def _on_spin_type_change(self, _): - if self.spin_type.value == "none": + if self._model.spin_type == "collinear" and self._model.has_tags: + self.warning.layout.display = "flex" self.magnetization_info.layout.visibility = "hidden" else: + self.warning.layout.display = "none" self.magnetization_info.layout.visibility = "visible" 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/common/mixins.py b/src/aiidalab_qe/common/mixins.py index fa43a1632..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): From 09e58291da86ab48b61c5c2ca76f84eafa474199 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sun, 29 Dec 2024 17:10:22 +0000 Subject: [PATCH 05/13] Fix layout --- .../app/configuration/advanced/magnetization/magnetization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py index 99801e754..47981fb3c 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py @@ -45,7 +45,7 @@ def render(self): return self.description = ipw.HTML(""" -
+
Magnetization:
The default starting magnetization is computed as the theoretical @@ -59,6 +59,7 @@ def render(self): "description_width": "initial", "button_width": "initial", }, + layout=ipw.Layout(margin="0 0 10px 0"), ) ipw.dlink( (self._model, "type_options"), From 681d87d897a3651f60238411028de483d62df0d8 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sun, 29 Dec 2024 17:13:42 +0000 Subject: [PATCH 06/13] Rescale starting magnetization down to magnetic moments --- .../advanced/magnetization/magnetization.py | 10 ++++------ .../advanced/magnetization/model.py | 16 +++++++++++----- src/aiidalab_qe/app/configuration/basic/basic.py | 2 +- src/aiidalab_qe/utils.py | 6 ++++-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py index 47981fb3c..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 @@ -48,9 +48,7 @@ def render(self):
Magnetization:
- The default starting magnetization is computed as the theoretical - magnetic moment scaled by the valence charge defined in the selected - pseudopotential family. + Default magnetic moments correspond to theoretical values.
""") diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py index 4d30c4b61..41c84d301 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py @@ -30,7 +30,7 @@ class MagnetizationConfigurationSettingsModel( type_options = tl.List( trait=tl.List(tl.Unicode()), default_value=[ - ["Starting magnetization", "starting_magnetization"], + ["Magnetic moments", "starting_magnetization"], ["Total magnetization", "tot_magnetization"], ], ) @@ -47,10 +47,16 @@ def update(self, specific=""): # noqa: ARG002 self._defaults["moments"] = {} else: try: - self._defaults["moments"] = get_starting_magnetization( - self.input_structure, - fetch_pseudo_family_by_label(self.family), - ) + 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 diff --git a/src/aiidalab_qe/app/configuration/basic/basic.py b/src/aiidalab_qe/app/configuration/basic/basic.py index ca0e1588b..120cc9c42 100644 --- a/src/aiidalab_qe/app/configuration/basic/basic.py +++ b/src/aiidalab_qe/app/configuration/basic/basic.py @@ -95,7 +95,7 @@ def render(self):

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

diff --git a/src/aiidalab_qe/utils.py b/src/aiidalab_qe/utils.py index 0bcc1dca3..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 @@ -32,6 +34,6 @@ def enable_pencil_decomposition(component): component.settings = orm.Dict({"CMDLINE": ["-pd", ".true."]}) -def fetch_pseudo_family_by_label(label): +def fetch_pseudo_family_by_label(label) -> PseudoPotentialFamily: """Fetch the pseudo family by label.""" - return orm.Group.collection.get(label=label) + return orm.Group.collection.get(label=label) # type: ignore From 7cf24b12cb7d8bd71e0cded9a06218c27df4a441 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sun, 29 Dec 2024 17:27:22 +0000 Subject: [PATCH 07/13] Fix basic settings magnetization notifications logic --- src/aiidalab_qe/app/configuration/basic/basic.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/basic/basic.py b/src/aiidalab_qe/app/configuration/basic/basic.py index 120cc9c42..9d393d695 100644 --- a/src/aiidalab_qe/app/configuration/basic/basic.py +++ b/src/aiidalab_qe/app/configuration/basic/basic.py @@ -56,7 +56,7 @@ def render(self): Set the desired magnetic configuration in advanced settings
""", - layout=ipw.Layout(visibility="hidden"), + layout=ipw.Layout(display="none"), ) # Spin-Orbit calculation @@ -186,9 +186,10 @@ def _on_input_structure_change(self, _): self.refresh(specific="structure") def _on_spin_type_change(self, _): - if self._model.spin_type == "collinear" and self._model.has_tags: - self.warning.layout.display = "flex" - 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.display = "none" self.warning.layout.display = "none" - self.magnetization_info.layout.visibility = "visible" From 782ab8cd84ed2239c053168c077fa14fdd38ab9a Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 9 Jan 2025 08:49:47 +0000 Subject: [PATCH 08/13] Add help text --- .../advanced/magnetization/magnetization.py | 53 +++++++++++---- .../advanced/magnetization/model.py | 67 ++++++++++++++----- 2 files changed, 91 insertions(+), 29 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py index f9bd79904..0ae6fcc5c 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py @@ -44,13 +44,18 @@ def render(self): if self.rendered: return - self.description = ipw.HTML(""" -
- Magnetization: -
- Default magnetic moments correspond to theoretical values. -
- """) + self.header = ipw.HTML("Magnetization:") + + self.unit = ipw.HTML( + value="ยตB", + layout=ipw.Layout(margin="2px 2px 5px"), + ) + + self.magnetization_type_help = ipw.HTML() + ipw.dlink( + (self._model, "type_help"), + (self.magnetization_type_help, "value"), + ) self.magnetization_type = ipw.ToggleButtons( style={ @@ -80,11 +85,19 @@ def render(self): (self.tot_magnetization, "value"), ) + self.tot_magnetization_with_unit = ipw.HBox( + children=[ + self.tot_magnetization, + self.unit, + ], + layout=ipw.Layout(align_items="center"), + ) + self.kind_moment_widgets = ipw.VBox() self.container = ipw.VBox( children=[ - self.tot_magnetization, + self.tot_magnetization_with_unit, ] ) @@ -99,12 +112,14 @@ def _on_input_structure_change(self, _): def _on_electronic_type_change(self, _): self._switch_widgets() + self._model.update_type_help() def _on_spin_type_change(self, _): self.refresh(specific="spin") def _on_magnetization_type_change(self, _): self._toggle_widgets() + self._model.update_type_help() def _update(self, specific=""): if self.updated: @@ -152,7 +167,15 @@ def _build_kinds_widget(self): ], ) self.links.append(link) - children.append(kind_moment_widget) + children.append( + ipw.HBox( + children=[ + kind_moment_widget, + self.unit, + ], + layout=ipw.Layout(align_items="center"), + ) + ) self.kind_moment_widgets.children = children @@ -162,23 +185,29 @@ def _switch_widgets(self): if self._model.spin_type == "none": children = [] else: - children = [self.description] + children = [self.header] if self._model.electronic_type == "metal": children.extend( [ self.magnetization_type, + self.magnetization_type_help, self.container, ] ) else: - children.append(self.tot_magnetization) + children.extend( + [ + self.magnetization_type_help, + self.tot_magnetization_with_unit, + ], + ) self.children = children def _toggle_widgets(self): if self._model.spin_type == "none" or not self.rendered: return self.container.children = [ - self.tot_magnetization + self.tot_magnetization_with_unit if self._model.type == "tot_magnetization" else self.kind_moment_widgets ] diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py index 41c84d301..9e60b88ef 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py @@ -30,11 +30,12 @@ class MagnetizationConfigurationSettingsModel( type_options = tl.List( trait=tl.List(tl.Unicode()), default_value=[ - ["Magnetic moments", "starting_magnetization"], + ["Initial magnetic moments", "starting_magnetization"], ["Total magnetization", "tot_magnetization"], ], ) type = tl.Unicode("starting_magnetization") + type_help = tl.Unicode("") total = tl.Float(0.0) moments = tl.Dict( key_trait=tl.Unicode(), # kind name @@ -42,34 +43,66 @@ class MagnetizationConfigurationSettingsModel( default_value={}, ) + _TYPE_HELP_TEMPLATE = """ +
+ {content} +
+ """ + def update(self, specific=""): # noqa: ARG002 if self.spin_type == "none" or not self.has_structure: self._defaults["moments"] = {} else: - 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() - } + self.update_type_help() + self._update_default_moments() with self.hold_trait_notifications(): self.moments = self._get_default_moments() + def update_type_help(self): + if self.electronic_type == "insulator" or self.type == "tot_magnetization": + self.type_help = self._TYPE_HELP_TEMPLATE.format( + content=""" + Constrain the desired total electronic magnetization (difference + between majority and minority spin charge). + """ + ) + else: + self.type_help = self._TYPE_HELP_TEMPLATE.format( + content=""" + If a nonzero ground-state magnetization is expected, you + must assign a nonzero value to at least one atomic + type (note that the app might already provide tentative initial + values to chemical elements that typically display magnetism). + To simulate an antiferromagnetic state, first, if you have not + done so already, please use the atom tag editor (Select + structure -> Edit structure -> Edit atom tags) to mark atoms of + the species of interest as distinct by assigning each a different + integer tag. Once tagged, assign each an initial magnetic moments + of opposite sign. + """ + ) + def reset(self): with self.hold_trait_notifications(): self.type = self.traits()["type"].default_value self.total = self.traits()["total"].default_value self.moments = self._get_default_moments() + def _update_default_moments(self): + 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() + } + def _get_default_moments(self): return deepcopy(self._defaults.get("moments", {})) From 539b5132a3e7b47e092adf233fd4d283c98d1aae Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 9 Jan 2025 10:17:31 +0000 Subject: [PATCH 09/13] Slight refactoring --- .../advanced/magnetization/model.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py index 9e60b88ef..1601d437a 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py @@ -1,6 +1,7 @@ from copy import deepcopy import traitlets as tl +from aiida_pseudo.groups.family import PseudoPotentialFamily from aiida.common.exceptions import NotExistent from aiida_quantumespresso.workflows.protocols.utils import get_starting_magnetization @@ -91,12 +92,9 @@ def reset(self): def _update_default_moments(self): try: family = fetch_pseudo_family_by_label(self.family) - initial_guess = get_starting_magnetization(self.input_structure, family) + 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, - ) + kind.name: self._to_moment(kind.symbol, guess[kind.name], family) for kind in self.input_structure.kinds } except NotExistent: @@ -104,5 +102,13 @@ def _update_default_moments(self): kind_name: 0.0 for kind_name in self.input_structure.get_kind_names() } + @staticmethod + def _to_moment( + symbol: str, + magnetization: float, + family: PseudoPotentialFamily, + ): + return round(magnetization * family.get_pseudo(symbol).z_valence, 3) + def _get_default_moments(self): return deepcopy(self._defaults.get("moments", {})) From ce19bcec0fece7462dfba7a4c19ff60395330a22 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 9 Jan 2025 11:43:58 +0000 Subject: [PATCH 10/13] Correct magnetic moment defaults --- .../advanced/magnetization/magnetization.py | 11 +++- .../advanced/magnetization/model.py | 53 +++++++++++-------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py index 0ae6fcc5c..681d8ad9e 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/magnetization.py @@ -39,6 +39,10 @@ def __init__(self, model: MagnetizationConfigurationSettingsModel, **kwargs): self._on_magnetization_type_change, "type", ) + self._model.observe( + self._on_family_change, + "family", + ) def render(self): if self.rendered: @@ -121,6 +125,9 @@ def _on_magnetization_type_change(self, _): self._toggle_widgets() self._model.update_type_help() + def _on_family_change(self, _): + self._model.update_default_starting_magnetization() + def _update(self, specific=""): if self.updated: return @@ -151,8 +158,8 @@ def _build_kinds_widget(self): for kind_name in kind_names: kind_moment_widget = ipw.BoundedFloatText( description=kind_name, - min=-4, - max=4, + min=-7, + max=7, step=0.1, ) link = ipw.link( diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py index 1601d437a..e5c048607 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py @@ -3,8 +3,10 @@ import traitlets as tl from aiida_pseudo.groups.family import PseudoPotentialFamily -from aiida.common.exceptions import NotExistent -from aiida_quantumespresso.workflows.protocols.utils import get_starting_magnetization +from aiida_quantumespresso.workflows.protocols.utils import ( + get_magnetization_parameters, + get_starting_magnetization, +) from aiidalab_qe.common.mixins import HasInputStructure from aiidalab_qe.utils import fetch_pseudo_family_by_label @@ -50,11 +52,18 @@ class MagnetizationConfigurationSettingsModel(
""" + _DEFAULT_MOMENTS = get_magnetization_parameters() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._default_starting_magnetization = {} + def update(self, specific=""): # noqa: ARG002 if self.spin_type == "none" or not self.has_structure: self._defaults["moments"] = {} else: self.update_type_help() + self.update_default_starting_magnetization() self._update_default_moments() with self.hold_trait_notifications(): self.moments = self._get_default_moments() @@ -83,6 +92,16 @@ def update_type_help(self): """ ) + def update_default_starting_magnetization(self): + if not self.has_structure: + # TODO this guard shouldn't be here! It IS here only because in the present + # implementation, an update is called on app start. This breaks lazy loading + # and should be carefully checked! + return + family = fetch_pseudo_family_by_label(self.family) + initial_guess = get_starting_magnetization(self.input_structure, family) + self._default_starting_magnetization = initial_guess + def reset(self): with self.hold_trait_notifications(): self.type = self.traits()["type"].default_value @@ -90,25 +109,17 @@ def reset(self): self.moments = self._get_default_moments() def _update_default_moments(self): - try: - family = fetch_pseudo_family_by_label(self.family) - guess = get_starting_magnetization(self.input_structure, family) - self._defaults["moments"] = { - kind.name: self._to_moment(kind.symbol, guess[kind.name], family) - 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() - } - - @staticmethod - def _to_moment( - symbol: str, - magnetization: float, - family: PseudoPotentialFamily, - ): - return round(magnetization * family.get_pseudo(symbol).z_valence, 3) + family = fetch_pseudo_family_by_label(self.family) + self._defaults["moments"] = { + kind.name: self._to_moment(symbol=kind.symbol, family=family) + for kind in self.input_structure.kinds + } + + def _to_moment(self, symbol: str, family: PseudoPotentialFamily) -> float: + magnetization = self._default_starting_magnetization.get(symbol, 0.0) + if self._DEFAULT_MOMENTS.get(symbol, {}).get("magmom"): + return magnetization * family.get_pseudo(symbol).z_valence + return 0.0 def _get_default_moments(self): return deepcopy(self._defaults.get("moments", {})) From a67aae23a2d587396eadea2b67aa3e74f195fbe9 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 9 Jan 2025 12:17:36 +0000 Subject: [PATCH 11/13] Enforce 0.1 magnetization default for non-magnetic species --- .../app/configuration/advanced/magnetization/model.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py index e5c048607..829655ae7 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py @@ -116,10 +116,12 @@ def _update_default_moments(self): } def _to_moment(self, symbol: str, family: PseudoPotentialFamily) -> float: - magnetization = self._default_starting_magnetization.get(symbol, 0.0) - if self._DEFAULT_MOMENTS.get(symbol, {}).get("magmom"): - return magnetization * family.get_pseudo(symbol).z_valence - return 0.0 + magnetization = ( + self._default_starting_magnetization.get(symbol, 0.1) + if self._DEFAULT_MOMENTS.get(symbol, {}).get("magmom") + else 0.1 + ) + return round(magnetization * family.get_pseudo(symbol).z_valence, 3) def _get_default_moments(self): return deepcopy(self._defaults.get("moments", {})) From 8a3218e9473a1bc52d63453721cc37a9da836012 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 9 Jan 2025 17:28:25 +0000 Subject: [PATCH 12/13] Update text --- .../app/configuration/advanced/magnetization/model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py index 829655ae7..4960b9dd8 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py @@ -81,8 +81,7 @@ def update_type_help(self): content=""" If a nonzero ground-state magnetization is expected, you must assign a nonzero value to at least one atomic - type (note that the app might already provide tentative initial - values to chemical elements that typically display magnetism). + type (note that the app already provide tentative initial values). To simulate an antiferromagnetic state, first, if you have not done so already, please use the atom tag editor (Select structure -> Edit structure -> Edit atom tags) to mark atoms of From 6c99573c694acaada52e66170356baca533687fc Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 9 Jan 2025 17:32:57 +0000 Subject: [PATCH 13/13] Add some documentation --- .../app/configuration/advanced/magnetization/model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py index 4960b9dd8..dcf3ef704 100644 --- a/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py +++ b/src/aiidalab_qe/app/configuration/advanced/magnetization/model.py @@ -69,6 +69,7 @@ def update(self, specific=""): # noqa: ARG002 self.moments = self._get_default_moments() def update_type_help(self): + """Update the type field help text w.r.t the current model state.""" if self.electronic_type == "insulator" or self.type == "tot_magnetization": self.type_help = self._TYPE_HELP_TEMPLATE.format( content=""" @@ -92,6 +93,9 @@ def update_type_help(self): ) def update_default_starting_magnetization(self): + """Update the default starting magnetization based on the structure and + pseudopotential family. + """ if not self.has_structure: # TODO this guard shouldn't be here! It IS here only because in the present # implementation, an update is called on app start. This breaks lazy loading @@ -115,6 +119,7 @@ def _update_default_moments(self): } def _to_moment(self, symbol: str, family: PseudoPotentialFamily) -> float: + """Convert the default magnetization to an initial magnetic moment.""" magnetization = ( self._default_starting_magnetization.get(symbol, 0.1) if self._DEFAULT_MOMENTS.get(symbol, {}).get("magmom")