From 2c5ca5543ed3f63707936e3dcc2b3610774b46c8 Mon Sep 17 00:00:00 2001 From: AndresOrtegaGuerrero <34098967+AndresOrtegaGuerrero@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:43:53 +0200 Subject: [PATCH] Fix tot magnetization (#512) * updating Magnetization widget, and including logic for insulator * adapting logic of bands and pdos for bandspdoswidget * starting_magnetization as default --- src/aiidalab_qe/app/configuration/advanced.py | 146 ++++++++++++++---- src/aiidalab_qe/app/result/summary_viewer.py | 4 + .../app/static/workflow_summary.jinja | 8 + src/aiidalab_qe/common/bandpdoswidget.py | 115 +++++++++++--- src/aiidalab_qe/plugins/bands/workchain.py | 5 + src/aiidalab_qe/plugins/pdos/workchain.py | 7 + src/aiidalab_qe/workflows/__init__.py | 9 ++ tests/conftest.py | 20 ++- tests/test_result.py | 2 +- tests/test_result/test_summary_report.yml | 1 + 10 files changed, 260 insertions(+), 57 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/advanced.py b/src/aiidalab_qe/app/configuration/advanced.py index 0140685f2..ad2b33264 100644 --- a/src/aiidalab_qe/app/configuration/advanced.py +++ b/src/aiidalab_qe/app/configuration/advanced.py @@ -211,6 +211,11 @@ def _update_input_structure(self, change): self.magnetization.input_structure = None self.pseudo_setter.structure = None + @tl.observe("electronic_type") + def _electronic_type_changed(self, change): + """Input electronic_type changed, update the widget values.""" + self.magnetization.electronic_type = change["new"] + @tl.observe("protocol") def _protocol_changed(self, _): """Input protocol changed, update the widget values.""" @@ -255,17 +260,16 @@ def get_panel_value(self): # XXX: start from parameters = {} and then bundle the settings by purposes (e.g. pw, bands, etc.) parameters = { "initial_magnetic_moments": None, - "pw": { - "parameters": { - "SYSTEM": {}, - }, - }, + "pw": {"parameters": {"SYSTEM": {}}}, + "clean_workdir": self.clean_workdir.value, + "pseudo_family": self.pseudo_family_selector.value, + "kpoints_distance": self.value.get("kpoints_distance"), } - # add clean_workdir to the parameters - parameters["clean_workdir"] = self.clean_workdir.value - # add the pseudo_family to the parameters - parameters["pseudo_family"] = self.pseudo_family_selector.value + # Set total charge + parameters["pw"]["parameters"]["SYSTEM"]["tot_charge"] = self.total_charge.value + + # Set the pseudos if self.pseudo_setter.pseudos: parameters["pw"]["pseudos"] = self.pseudo_setter.pseudos parameters["pw"]["parameters"]["SYSTEM"]["ecutwfc"] = ( @@ -303,8 +307,35 @@ def get_panel_value(self): self.smearing.degauss_value ) + # Set tot_magnetization for collinear simulations. + if self.spin_type == "collinear": + # Conditions for metallic systems. Select the magnetization type and set the value if override is True + if self.electronic_type == "metal" and self.override.value is True: + self.set_metallic_magnetization(parameters) + # Conditions for insulator systems. Default value is 0.0 + elif self.electronic_type == "insulator": + self.set_insulator_magnetization(parameters) + return parameters + def set_insulator_magnetization(self, parameters): + """Set the parameters for collinear insulator calculation. Total magnetization.""" + parameters["pw"]["parameters"]["SYSTEM"]["tot_magnetization"] = ( + self.magnetization.tot_magnetization.value + ) + + def set_metallic_magnetization(self, parameters): + """Set the parameters for magnetization calculation in metals""" + magnetization_type = self.magnetization.magnetization_type.value + if magnetization_type == "tot_magnetization": + parameters["pw"]["parameters"]["SYSTEM"]["tot_magnetization"] = ( + self.magnetization.tot_magnetization.value + ) + else: + parameters["initial_magnetic_moments"] = ( + self.magnetization.get_magnetization() + ) + def set_panel_value(self, parameters): """Set the panel value from the given parameters.""" @@ -338,11 +369,18 @@ def set_panel_value(self, parameters): parameters["pw"]["parameters"]["SYSTEM"].get("vdw_corr", "none"), ) + # Logic to set the magnetization if parameters.get("initial_magnetic_moments"): self.magnetization._set_magnetization_values( parameters.get("initial_magnetic_moments") ) + if "tot_magnetization" in parameters["pw"]["parameters"]["SYSTEM"]: + self.magnetization.magnetization_type.value = "tot_magnetization" + self.magnetization._set_tot_magnetization( + parameters["pw"]["parameters"]["SYSTEM"]["tot_magnetization"] + ) + def reset(self): """Reset the widget and the traitlets""" @@ -363,7 +401,11 @@ def reset(self): self.override.value = False self.smearing.reset() # reset the pseudo setter - self.pseudo_setter._reset() + if self.input_structure is None: + self.pseudo_setter.structure = None + self.pseudo_setter._reset() + else: + self.pseudo_setter._reset() # reset the magnetization self.magnetization.reset() # reset mesh grid @@ -387,10 +429,13 @@ def _display_mesh(self, _=None): class MagnetizationSettings(ipw.VBox): - """Widget to set the initial magnetic moments for each kind names defined in the StructureData (StructureDtaa.get_kind_names()) + """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. + + For Starting magnetization you can set each kind names defined in the StructureData (StructureDtaa.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 Attributes: @@ -398,30 +443,45 @@ class MagnetizationSettings(ipw.VBox): """ input_structure = tl.Instance(orm.StructureData, allow_none=True) - + electronic_type = tl.Unicode() disabled = tl.Bool() + _DEFAULT_TOT_MAGNETIZATION = 0.0 + _DEFAULT_DESCRIPTION = "Magnetization: Input structure not confirmed" def __init__(self, **kwargs): self.input_structure = orm.StructureData() self.input_structure_labels = [] - self.description = ipw.HTML( - "Define magnetization: Input structure not confirmed" + self.tot_magnetization = ipw.BoundedIntText( + min=0, + max=100, + step=1, + value=self._DEFAULT_TOT_MAGNETIZATION, + disabled=True, + description="Total magnetization:", + style={"description_width": "initial"}, + ) + self.magnetization_type = ipw.ToggleButtons( + options=[ + ("Starting Magnetization", "starting_magnetization"), + ("Tot. Magnetization", "tot_magnetization"), + ], + value="starting_magnetization", + style={"description_width": "initial"}, ) + self.description = ipw.HTML(self._DEFAULT_DESCRIPTION) self.kinds = self.create_kinds_widget() self.kinds_widget_out = ipw.Output() + self.magnetization_out = ipw.Output() + self.magnetization_type.observe(self._render, "value") super().__init__( children=[ - ipw.HBox( - [ - self.description, - self.kinds_widget_out, - ], - ), + self.description, + self.magnetization_out, + self.kinds_widget_out, ], layout=ipw.Layout(justify_content="space-between"), **kwargs, ) - self.display_kinds() @tl.observe("disabled") def _disabled_changed(self, _): @@ -429,19 +489,19 @@ def _disabled_changed(self, _): if hasattr(self.kinds, "children") and self.kinds.children: for i in range(len(self.kinds.children)): self.kinds.children[i].disabled = self.disabled + self.tot_magnetization.disabled = self.disabled + self.magnetization_type.disabled = self.disabled def reset(self): self.disabled = True + self.tot_magnetization.value = self._DEFAULT_TOT_MAGNETIZATION + # if self.input_structure is None: - self.description.value = ( - "Define magnetization: Input structure not confirmed" - ) + self.description.value = self._DEFAULT_DESCRIPTION self.kinds = None - with self.kinds_widget_out: - clear_output() - else: - self.update_kinds_widget() + self.description.value = "Magnetization" + self.kinds = self.create_kinds_widget() def create_kinds_widget(self): if self.input_structure_labels: @@ -462,11 +522,30 @@ def create_kinds_widget(self): return kinds_widget + @tl.observe("electronic_type") + def _electronic_type_changed(self, change): + with self.magnetization_out: + clear_output() + if change["new"] == "metal": + display(self.magnetization_type) + self._render({"new": self.magnetization_type.value}) + else: + display(self.tot_magnetization) + with self.kinds_widget_out: + clear_output() + def update_kinds_widget(self): self.input_structure_labels = self.input_structure.get_kind_names() self.kinds = self.create_kinds_widget() - self.description.value = "Define magnetization: " - self.display_kinds() + self.description.value = "Magnetization" + + def _render(self, value): + if value["new"] == "tot_magnetization": + with self.kinds_widget_out: + clear_output() + display(self.tot_magnetization) + else: + self.display_kinds() def display_kinds(self): if "PYTEST_CURRENT_TEST" not in os.environ and self.kinds: @@ -477,6 +556,7 @@ def display_kinds(self): def _update_widget(self, change): self.input_structure = change["new"] self.update_kinds_widget() + self.display_kinds() def get_magnetization(self): """Method to generate the dictionary with the initial magnetic moments""" @@ -497,6 +577,10 @@ def _set_magnetization_values(self, magnetic_moments): else: self.kinds.children[i].value = magnetic_moments + def _set_tot_magnetization(self, tot_magnetization): + """Set the total magnetization""" + self.tot_magnetization.value = tot_magnetization + class SmearingSettings(ipw.VBox): # accept protocol as input and set the values diff --git a/src/aiidalab_qe/app/result/summary_viewer.py b/src/aiidalab_qe/app/result/summary_viewer.py index 59f12e2c6..95840dd00 100644 --- a/src/aiidalab_qe/app/result/summary_viewer.py +++ b/src/aiidalab_qe/app/result/summary_viewer.py @@ -123,6 +123,10 @@ def generate_report_parameters(qeapp_wc): report["periodicity"] = PERIODICITY_MAPPING.get( qeapp_wc.inputs.structure.pbc, "xyz" ) + report["tot_magnetization"] = pw_parameters["SYSTEM"].get( + "tot_magnetization", False + ) + # hard code bands and pdos if "bands" in qeapp_wc.inputs: report["bands_kpoints_distance"] = PwBandsWorkChain.get_protocol_inputs( diff --git a/src/aiidalab_qe/app/static/workflow_summary.jinja b/src/aiidalab_qe/app/static/workflow_summary.jinja index 33630127d..dd54e3718 100644 --- a/src/aiidalab_qe/app/static/workflow_summary.jinja +++ b/src/aiidalab_qe/app/static/workflow_summary.jinja @@ -104,10 +104,18 @@ Van der Waals Correction {{ vdw_corr }} + + {% if tot_magnetization %} + + Total magnetization + {{ tot_magnetization }} + + {% else %} Initial Magnetic Moments {{ initial_magnetic_moments }} + {% endif %} diff --git a/src/aiidalab_qe/common/bandpdoswidget.py b/src/aiidalab_qe/common/bandpdoswidget.py index 540c2c647..04d43a1b5 100644 --- a/src/aiidalab_qe/common/bandpdoswidget.py +++ b/src/aiidalab_qe/common/bandpdoswidget.py @@ -43,12 +43,21 @@ def __init__(self, bands_data=None, pdos_data=None): self._dos_yaxis = self._dos_yaxis() def _get_fermi_energy(self): - fermi_energy = ( - self.pdos_data["fermi_energy"] - if self.pdos_data - else self.bands_data["fermi_energy"] - ) - return fermi_energy + """Function to return the Fermi energy information depending on the data available.""" + fermi_data = {} + if self.pdos_data: + if "fermi_energy_up" in self.pdos_data: + fermi_data["fermi_energy_up"] = self.pdos_data["fermi_energy_up"] + fermi_data["fermi_energy_down"] = self.pdos_data["fermi_energy_down"] + else: + fermi_data["fermi_energy"] = self.pdos_data["fermi_energy"] + else: + if "fermi_energy_up" in self.bands_data: + fermi_data["fermi_energy_up"] = self.bands_data["fermi_energy_up"] + fermi_data["fermi_energy_down"] = self.bands_data["fermi_energy_down"] + else: + fermi_data["fermi_energy"] = self.bands_data["fermi_energy"] + return fermi_data def _band_xaxis(self): """Function to return the xaxis for the bands plot.""" @@ -267,7 +276,7 @@ def _add_band_traces(self, fig, paths, plot_type): scatter_objects.append( go.Scatter( x=band["x"], - y=bands_np - self.fermi_energy, + y=bands_np - self.fermi_energy["fermi_energy"], mode="lines", line=dict( color=self.SETTINGS["bands_linecolor"], @@ -286,16 +295,15 @@ def _add_band_traces(self, fig, paths, plot_type): color_first_half = self.SETTINGS["bands_up_linecolor"] # Blue line for the Spin down color_second_half = self.SETTINGS["bands_down_linecolor"] - - for bands, color in zip( - (first_half, second_half), (color_first_half, color_second_half) - ): - for band_values in bands: - bands_np = np.array(band_values) + if "fermi_energy" in self.fermi_energy: + for bands, color in zip( + (first_half, second_half), (color_first_half, color_second_half) + ): + bands_np = np.array(bands) scatter_objects.append( go.Scatter( x=band["x"], - y=bands_np - self.fermi_energy, + y=bands_np - self.fermi_energy["fermi_energy"], mode="lines", line=dict( color=color, @@ -305,6 +313,30 @@ def _add_band_traces(self, fig, paths, plot_type): showlegend=False, ) ) + else: + for bands, color, fermi_energy in zip( + (first_half, second_half), + (color_first_half, color_second_half), + ( + self.fermi_energy["fermi_energy_up"], + self.fermi_energy["fermi_energy_down"], + ), + ): + for band_values in bands: + bands_np = np.array(band_values) + scatter_objects.append( + go.Scatter( + x=band["x"], + y=bands_np - fermi_energy, + mode="lines", + line=dict( + color=color, + shape="spline", + smoothing=1.3, + ), + showlegend=False, + ) + ) if plot_type == "bands_only": fig.add_traces(scatter_objects) @@ -325,12 +357,41 @@ def _add_dos_traces(self, fig, plot_type): for i, trace in enumerate(dos_data): dos_np = np.array(trace["x"]) fill = "tozerox" if plot_type == "combined" else "tozeroy" - x_data = ( - trace["y"] if plot_type == "combined" else dos_np - self.fermi_energy - ) - y_data = ( - dos_np - self.fermi_energy if plot_type == "combined" else trace["y"] - ) + + if "fermi_energy" in self.fermi_energy: + y_data = ( + dos_np - self.fermi_energy["fermi_energy"] + if plot_type == "combined" + else trace["y"] + ) + x_data = ( + trace["y"] + if plot_type == "combined" + else dos_np - self.fermi_energy["fermi_energy"] + ) + else: + if trace["label"].endswith("(↑)"): + y_data = ( + dos_np - self.fermi_energy["fermi_energy_up"] + if plot_type == "combined" + else trace["y"] + ) + x_data = ( + trace["y"] + if plot_type == "combined" + else dos_np - self.fermi_energy["fermi_energy_up"] + ) + else: + y_data = ( + dos_np - self.fermi_energy["fermi_energy_down"] + if plot_type == "combined" + else trace["y"] + ) + x_data = ( + trace["y"] + if plot_type == "combined" + else dos_np - self.fermi_energy["fermi_energy_down"] + ) scatter_objects[i] = go.Scatter( x=x_data, y=y_data, @@ -632,9 +693,15 @@ def get_pdos_data(pdos, group_tag, plot_tag, selected_atoms): ) data_dict = { - "fermi_energy": pdos.nscf.output_parameters["fermi_energy"], "dos": dos, } + if "fermi_energy_up" in pdos.nscf.output_parameters: + data_dict["fermi_energy_up"] = pdos.nscf.output_parameters["fermi_energy_up"] + data_dict["fermi_energy_down"] = pdos.nscf.output_parameters[ + "fermi_energy_down" + ] + else: + data_dict["fermi_energy"] = pdos.nscf.output_parameters["fermi_energy"] return json.loads(json.dumps(data_dict)) @@ -802,7 +869,11 @@ def export_bands_data(outputs, fermi_energy=None): data = json.loads(outputs.band_structure._exportcontent("json", comments=False)[0]) # The fermi energy from band calculation is not robust. - data["fermi_energy"] = outputs.band_parameters["fermi_energy"] or fermi_energy + if "fermi_energy_up" in outputs.band_parameters: + data["fermi_energy_up"] = outputs.band_parameters["fermi_energy_up"] + data["fermi_energy_down"] = outputs.band_parameters["fermi_energy_down"] + else: + data["fermi_energy"] = outputs.band_parameters["fermi_energy"] or fermi_energy data["pathlabels"] = get_bands_labeling(data) return data diff --git a/src/aiidalab_qe/plugins/bands/workchain.py b/src/aiidalab_qe/plugins/bands/workchain.py index 484ec082f..e60c611f4 100644 --- a/src/aiidalab_qe/plugins/bands/workchain.py +++ b/src/aiidalab_qe/plugins/bands/workchain.py @@ -217,6 +217,11 @@ def get_builder(codes, structure, parameters, **kwargs): bands.pop("relax") bands.pop("structure", None) bands.pop("clean_workdir", None) + + if scf_overrides["pw"]["parameters"]["SYSTEM"].get("tot_magnetization") is not None: + bands.scf["pw"]["parameters"]["SYSTEM"].pop("starting_magnetization", None) + bands.bands["pw"]["parameters"]["SYSTEM"].pop("starting_magnetization", None) + return bands diff --git a/src/aiidalab_qe/plugins/pdos/workchain.py b/src/aiidalab_qe/plugins/pdos/workchain.py index bd9915f2e..b0c8325e7 100644 --- a/src/aiidalab_qe/plugins/pdos/workchain.py +++ b/src/aiidalab_qe/plugins/pdos/workchain.py @@ -66,6 +66,13 @@ def get_builder(codes, structure, parameters, **kwargs): # pop the inputs that are exclueded from the expose_inputs pdos.pop("structure", None) pdos.pop("clean_workdir", None) + + if ( + scf_overrides["pw"]["parameters"]["SYSTEM"].get("tot_magnetization") + is not None + ): + pdos.scf["pw"]["parameters"]["SYSTEM"].pop("starting_magnetization", None) + pdos.nscf["pw"]["parameters"]["SYSTEM"].pop("starting_magnetization", None) else: raise ValueError("The dos_code and projwfc_code are required.") return pdos diff --git a/src/aiidalab_qe/workflows/__init__.py b/src/aiidalab_qe/workflows/__init__.py index ac292cb4a..c2d002815 100644 --- a/src/aiidalab_qe/workflows/__init__.py +++ b/src/aiidalab_qe/workflows/__init__.py @@ -146,6 +146,15 @@ def get_builder_from_protocol( relax_builder.pop("base_final_scf", None) # never run a final scf builder.relax = relax_builder + # remove starting magnetization if tot_magnetization is set + if ( + relax_builder["base"]["pw"]["parameters"]["SYSTEM"].get("tot_magnetization") + is not None + ): + builder.relax["base"]["pw"]["parameters"]["SYSTEM"].pop( + "starting_magnetization", None + ) + if properties is None: properties = [] builder.properties = orm.List(list=properties) diff --git a/tests/conftest.py b/tests/conftest.py index a83ac32cf..cca1ed0d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -603,7 +603,10 @@ def _generate_qeapp_workchain( run_bands=True, run_pdos=True, spin_type="none", + electronic_type="metal", + magnetization_type="starting_magnetization", # Options: "starting_magnetization", "tot_magnetization" initial_magnetic_moments=0.0, + tot_magnetization=0.0, ): from copy import deepcopy @@ -635,9 +638,20 @@ def _generate_qeapp_workchain( s2.workchain_settings.properties["pdos"].run.value = run_pdos s2.workchain_settings.workchain_protocol.value = "fast" s2.workchain_settings.spin_type.value = spin_type - s2.advanced_settings.magnetization._set_magnetization_values( - initial_magnetic_moments - ) + s2.workchain_settings.electronic_type.value = electronic_type + if spin_type == "collinear": + s2.advanced_settings.override.value = True + magnetization_values = ( + initial_magnetic_moments + if magnetization_type == "starting_magnetization" + else tot_magnetization + ) + s2.advanced_settings.magnetization._set_tot_magnetization( + tot_magnetization + ) if electronic_type == "insulator" else s2.advanced_settings.magnetization._set_magnetization_values( + magnetization_values + ) + s2.confirm() # step 3 setup code and resources s3: SubmitQeAppWorkChainStep = app.submit_step diff --git a/tests/test_result.py b/tests/test_result.py index 7fca7ba85..c6e087d17 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -34,7 +34,7 @@ def test_summary_report_advanced_settings(data_regression, generate_qeapp_workch from aiidalab_qe.app.result.summary_viewer import SummaryView wkchain = generate_qeapp_workchain( - spin_type="collinear", initial_magnetic_moments=0.1 + spin_type="collinear", electronic_type="metal", initial_magnetic_moments=0.1 ) viewer = SummaryView(wkchain.node) report = viewer.report diff --git a/tests/test_result/test_summary_report.yml b/tests/test_result/test_summary_report.yml index 734ec9e11..d41f91b10 100644 --- a/tests/test_result/test_summary_report.yml +++ b/tests/test_result/test_summary_report.yml @@ -27,4 +27,5 @@ relaxed: positions_cell scf_kpoints_distance: 0.5 smearing: cold tot_charge: 0.0 +tot_magnetization: false vdw_corr: none