diff --git a/docs/source/development/architecture.rst b/docs/source/development/architecture.rst index 4daf5c908..5627b6c78 100644 --- a/docs/source/development/architecture.rst +++ b/docs/source/development/architecture.rst @@ -49,7 +49,7 @@ The dictionary has the following structure: "pseudo_family": "SSSP/1.3/PBEsol/efficiency", "kpoints_distance": 0.5, }, - "bands": {"kpath_2d": "hexagonal"}, + "bands": {}, "pdos": {...}, "plugin_1": {...}, "plugin_2": {...}, diff --git a/docs/source/development/plugin.rst b/docs/source/development/plugin.rst index 9176e69fd..5199e70cf 100644 --- a/docs/source/development/plugin.rst +++ b/docs/source/development/plugin.rst @@ -210,7 +210,7 @@ The `parameters` passed to the `get_builder` function has the following structur "pseudo_family": "SSSP/1.3/PBEsol/efficiency", "kpoints_distance": 0.5, }, - "bands": {"kpath_2d": "hexagonal"}, + "bands": {}, "pdos": {...}, "eos": {...}, "plugin_1": {...}, diff --git a/setup.cfg b/setup.cfg index 96b30824d..ae279ba08 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ install_requires = aiida-pseudo~=1.4 filelock~=3.8 importlib-resources~=5.2 + aiida-wannier90-workflows==2.3.0 python_requires = >=3.9 [options.packages.find] @@ -60,6 +61,9 @@ aiidalab_qe.properties = electronic_structure = aiidalab_qe.plugins.electronic_structure:electronic_structure xas = aiidalab_qe.plugins.xas:xas +aiida.workflows = + aiidalab_qe.bands_workchain = aiidalab_qe.plugins.bands.bands_workchain:BandsWorkChain + [aiidalab] title = Quantum ESPRESSO description = Perform Quantum ESPRESSO calculations diff --git a/src/aiidalab_qe/app/configuration/workflow.py b/src/aiidalab_qe/app/configuration/workflow.py index 47db49cc0..408906ee5 100644 --- a/src/aiidalab_qe/app/configuration/workflow.py +++ b/src/aiidalab_qe/app/configuration/workflow.py @@ -38,14 +38,6 @@ class WorkChainSettings(Panel): """<div style="padding-top: 0px; padding-bottom: 0px"> <h4>Properties</h4></div>""" ) - properties_help = ipw.HTML( - """<div style="line-height: 140%; padding-top: 10px; padding-bottom: 0px"> - The band structure workflow will - automatically detect the default path in reciprocal space using the - <a href="https://www.materialscloud.org/work/tools/seekpath" target="_blank"> - SeeK-path tool</a>.</div>""" - ) - protocol_title = ipw.HTML( """<div style="padding-top: 0px; padding-bottom: 0px"> <h4>Protocol</h4></div>""" @@ -116,7 +108,6 @@ def update_reminder_info(change, name=name): if name in setting_entries: self.properties[name].run.observe(update_reminder_info, "value") - self.property_children.append(self.properties_help) self.children = [ self.structure_title, self.structure_help, diff --git a/src/aiidalab_qe/app/parameters/qeapp.yaml b/src/aiidalab_qe/app/parameters/qeapp.yaml index c00c0f1c1..1fba8c032 100644 --- a/src/aiidalab_qe/app/parameters/qeapp.yaml +++ b/src/aiidalab_qe/app/parameters/qeapp.yaml @@ -29,6 +29,8 @@ codes: code: dos-7.2@localhost projwfc: code: projwfc-7.2@localhost + projwfc_bands: + code: projwfc-7.2@localhost pw: code: pw-7.2@localhost pp: diff --git a/src/aiidalab_qe/plugins/bands/__init__.py b/src/aiidalab_qe/plugins/bands/__init__.py index 3d67f0a7b..b38797141 100644 --- a/src/aiidalab_qe/plugins/bands/__init__.py +++ b/src/aiidalab_qe/plugins/bands/__init__.py @@ -1,5 +1,6 @@ # from aiidalab_qe.bands.result import Result from aiidalab_qe.common.panel import OutlinePanel +from aiidalab_qe.common.widgets import QEAppComputationalResourcesWidget from .result import Result from .setting import Setting @@ -15,8 +16,15 @@ class BandsOutline(OutlinePanel): """ +projwfc_code = QEAppComputationalResourcesWidget( + description="projwfc.x", + default_calc_job_plugin="quantumespresso.projwfc", +) + + bands = { "outline": BandsOutline, + "code": {"projwfc_bands": projwfc_code}, "setting": Setting, "result": Result, "workchain": workchain_and_builder, diff --git a/src/aiidalab_qe/plugins/bands/bands_workchain.py b/src/aiidalab_qe/plugins/bands/bands_workchain.py new file mode 100644 index 000000000..d5401341d --- /dev/null +++ b/src/aiidalab_qe/plugins/bands/bands_workchain.py @@ -0,0 +1,382 @@ +import math + +import numpy as np + +from aiida import orm +from aiida.common import AttributeDict +from aiida.engine import WorkChain +from aiida.plugins import DataFactory, WorkflowFactory + +GAMMA = "\u0393" + +PwBandsWorkChain = WorkflowFactory("quantumespresso.pw.bands") +ProjwfcBandsWorkChain = WorkflowFactory("wannier90_workflows.projwfcbands") +KpointsData = DataFactory("core.array.kpoints") + + +def points_per_branch(vector_a, vector_b, reciprocal_cell, bands_kpoints_distance): + """function to calculate the number of points per branch depending on the kpoints_distance and the reciprocal cell""" + scaled_vector_a = np.array(vector_a) + scaled_vector_b = np.array(vector_b) + reciprocal_vector_a = scaled_vector_a.dot(reciprocal_cell) + reciprocal_vector_b = scaled_vector_b.dot(reciprocal_cell) + distance = np.linalg.norm(reciprocal_vector_a - reciprocal_vector_b) + return max( + 2, int(np.round(distance / bands_kpoints_distance)) + ) # at least two points for each segment, including both endpoints explicitly + + +def calculate_bands_kpoints_distance(kpoints_distance): + """function to calculate the bands_kpoints_distance depending on the kpoints_distance""" + if kpoints_distance >= 0.5: + return 0.1 + elif 0.15 < kpoints_distance < 0.5: + return 0.025 + else: + return 0.015 + + +def generate_kpath_1d(structure, kpoints_distance): + """Return a kpoints object for one dimensional systems (from Gamma to X) + The number of kpoints is calculated based on the kpoints_distance (as in the PwBandsWorkChain protocol) + """ + kpoints = KpointsData() + kpoints.set_cell_from_structure(structure) + reciprocal_cell = kpoints.reciprocal_cell + bands_kpoints_distance = calculate_bands_kpoints_distance(kpoints_distance) + + # Number of points per branch + num_points_per_branch = points_per_branch( + [0.0, 0.0, 0.0], + [0.5, 0.0, 0.0], + reciprocal_cell, + bands_kpoints_distance, + ) + # Generate the kpoints + points = np.linspace( + start=[0.0, 0.0, 0.0], + stop=[0.5, 0.0, 0.0], + endpoint=True, + num=num_points_per_branch, + ) + kpoints.set_kpoints(points.tolist()) + kpoints.labels = [[0, GAMMA], [len(points) - 1, "X"]] + return kpoints + + +def generate_kpath_2d(structure, kpoints_distance, kpath_2d): + """Return a kpoints object for two dimensional systems based on the selected 2D symmetry path + The number of kpoints is calculated based on the kpoints_distance (as in the PwBandsWorkChain protocol) + The 2D symmetry paths are defined as in The Journal of Physical Chemistry Letters 2022 13 (50), 11581-11594 (https://pubs.acs.org/doi/10.1021/acs.jpclett.2c02972) + """ + kpoints = KpointsData() + kpoints.set_cell_from_structure(structure) + reciprocal_cell = kpoints.reciprocal_cell + bands_kpoints_distance = calculate_bands_kpoints_distance(kpoints_distance) + + # dictionary with the 2D symmetry paths + selected_paths = { + "hexagonal": { + "path": [ + [0.0, 0.0, 0.0], + [0.5, 0.0, 0.0], + [0.33333, 0.33333, 0.0], + [1.0, 0.0, 0.0], + ], + "labels": [GAMMA, "M", "K", GAMMA], + }, + "square": { + "path": [ + [0.0, 0.0, 0.0], + [0.5, 0.0, 0.0], + [0.5, 0.5, 0.0], + [1.0, 0.0, 0.0], + ], + "labels": [GAMMA, "X", "M", GAMMA], + }, + "rectangular": { + "path": [ + [0.0, 0.0, 0.0], + [0.5, 0.0, 0.0], + [0.5, 0.5, 0.0], + [0.0, 0.5, 0.0], + [1.0, 0.0, 0.0], + ], + "labels": [GAMMA, "X", "S", "Y", GAMMA], + }, + } + # if the selected path is centered_rectangular or oblique, the path is calculated based on the reciprocal cell + if kpath_2d in ["centered_rectangular", "oblique"]: + a1 = reciprocal_cell[0] + a2 = reciprocal_cell[1] + norm_a1 = np.linalg.norm(a1) + norm_a2 = np.linalg.norm(a2) + cos_gamma = ( + a1.dot(a2) / (norm_a1 * norm_a2) + ) # Angle between a1 and a2 # like in https://pubs.acs.org/doi/10.1021/acs.jpclett.2c02972 + gamma = np.arccos(cos_gamma) + eta = (1 - (norm_a1 / norm_a2) * cos_gamma) / (2 * np.power(np.sin(gamma), 2)) + nu = 0.5 - (eta * norm_a2 * cos_gamma) / norm_a1 + selected_paths["centered_rectangular"] = { + "path": [ + [0.0, 0.0, 0.0], + [0.5, 0.0, 0.0], + [1 - eta, nu, 0], + [0.5, 0.5, 0.0], + [eta, 1 - nu, 0.0], + [1.0, 0.0, 0.0], + ], + "labels": [GAMMA, "X", "H_1", "C", "H", GAMMA], + } + selected_paths["oblique"] = { + "path": [ + [0.0, 0.0, 0.0], + [0.5, 0.0, 0.0], + [1 - eta, nu, 0], + [0.5, 0.5, 0.0], + [eta, 1 - nu, 0.0], + [0.0, 0.5, 0.0], + [1.0, 0.0, 0.0], + ], + "labels": [GAMMA, "X", "H_1", "C", "H", "Y", GAMMA], + } + path = selected_paths[kpath_2d]["path"] + labels = selected_paths[kpath_2d]["labels"] + branches = zip(path[:-1], path[1:]) + + all_kpoints = [] # List to hold all k-points + label_map = [] # List to hold labels and their corresponding k-point indices + + # Calculate the number of points per branch and generate the kpoints + index_offset = 0 # Start index for each segment + for (start, end), label_start, _ in zip(branches, labels[:-1], labels[1:]): + num_points_per_branch = points_per_branch( + start, end, reciprocal_cell, bands_kpoints_distance + ) + # Exclude endpoint except for the last segment to prevent duplication + points = np.linspace(start, end, num=num_points_per_branch, endpoint=False) + all_kpoints.extend(points) + label_map.append( + (index_offset, label_start) + ) # Label for the start of the segment + index_offset += len(points) + + # Include the last point and its label + all_kpoints.append(path[-1]) + label_map.append((index_offset, labels[-1])) # Label for the last point + + # Set the kpoints and their labels in KpointsData + kpoints.set_kpoints(all_kpoints) + kpoints.labels = label_map + + return kpoints + + +def determine_symmetry_path(structure): + # Tolerance for checking equality + cell_lengths = structure.cell_lengths + cell_angles = structure.cell_angles + tolerance = 1e-3 + + # Define symmetry conditions and their corresponding types in a dictionary + symmetry_conditions = { + ( + math.isclose(cell_lengths[0], cell_lengths[1], abs_tol=tolerance) + and math.isclose(cell_angles[2], 120.0, abs_tol=tolerance) + ): "hexagonal", + ( + math.isclose(cell_lengths[0], cell_lengths[1], abs_tol=tolerance) + and math.isclose(cell_angles[2], 90.0, abs_tol=tolerance) + ): "square", + ( + not math.isclose(cell_lengths[0], cell_lengths[1], abs_tol=tolerance) + and math.isclose(cell_angles[2], 90.0, abs_tol=tolerance) + ): "rectangular", + ( + math.isclose( + cell_lengths[1] * math.cos(math.radians(cell_angles[2])), + cell_lengths[0] / 2, + abs_tol=tolerance, + ) + ): "rectangular_centered", + ( + not math.isclose(cell_lengths[0], cell_lengths[1], abs_tol=tolerance) + and not math.isclose(cell_angles[2], 90.0, abs_tol=tolerance) + ): "oblique", + } + + # Check for symmetry type based on conditions + for condition, symmetry_type in symmetry_conditions.items(): + if condition: + return symmetry_type + + raise ValueError("Invalid symmetry type") + + +class BandsWorkChain(WorkChain): + "Workchain to compute the electronic band structure" + + label = "bands" + + @classmethod + def define(cls, spec): + super().define(spec) + + spec.input("structure", valid_type=orm.StructureData) + spec.expose_inputs( + PwBandsWorkChain, + namespace="bands", + exclude=["structure", "relax"], + namespace_options={ + "required": False, + "populate_defaults": False, + "help": "Inputs for the `PwBandsWorkChain`, simulation mode normal.", + }, + ) + spec.expose_inputs( + ProjwfcBandsWorkChain, + namespace="bands_projwfc", + exclude=["structure", "relax"], + namespace_options={ + "required": False, + "populate_defaults": False, + "help": "Inputs for the `ProjwfcBandsWorkChain`, simulation mode fat_bands.", + }, + ) + + spec.expose_outputs( + PwBandsWorkChain, + namespace="bands", + namespace_options={ + "required": False, + "help": "Outputs of the `PwBandsWorkChain`.", + }, + ) + spec.expose_outputs( + ProjwfcBandsWorkChain, + namespace="bands_projwfc", + namespace_options={ + "required": False, + "help": "Outputs of the `PwBandsWorkChain`.", + }, + ) + + spec.outline(cls.setup, cls.run_bands, cls.results) + + spec.exit_code( + 400, "ERROR_WORKCHAIN_FAILED", message="The workchain bands failed." + ) + + @classmethod + def get_builder_from_protocol( + cls, + pw_code, + projwfc_code, + structure, + simulation_mode="normal", + protocol=None, + overrides=None, + **kwargs, + ): + """Return a BandsWorkChain builder prepopulated with inputs following the specified protocol + + :param structure: the ``StructureData`` instance to use. + :param pw_code: the ``Code`` instance configured for the ``quantumespresso.pw`` plugin. + :param protocol: protocol to use, if not specified, the default will be used. + :param projwfc_code: the ``Code`` instance configured for the ``quantumespresso.projwfc`` plugin. + :param simulation_mode: hat type of simulation to run normal band or fat bands. + + """ + + builder = cls.get_builder() + + if simulation_mode == "normal": + builder_bands = PwBandsWorkChain.get_builder_from_protocol( + pw_code, structure, protocol, overrides=overrides, **kwargs + ) + builder.pop("bands_projwfc", None) + builder_bands.pop("relax", None) + builder_bands.pop("structure", None) + builder.bands = builder_bands + + elif simulation_mode == "fat_bands": + builder_bands_projwfc = ProjwfcBandsWorkChain.get_builder_from_protocol( + pw_code, + projwfc_code, + structure, + protocol=protocol, + overrides=overrides, + **kwargs, + ) + builder.pop("bands", None) + builder_bands_projwfc.pop("relax", None) + builder_bands_projwfc.pop("structure", None) + builder.bands_projwfc = builder_bands_projwfc + + else: + raise ValueError(f"Unknown simulation_mode: {simulation_mode}") + + # Handle periodic boundary conditions (PBC) + if structure.pbc != (True, True, True): + kpoints_distance = overrides.get("scf", {}).get( + "kpoints_distance", + builder.get("bands", {}).get("scf", {}).get("kpoints_distance"), + ) + kpoints = None + if structure.pbc == (True, False, False): + kpoints = generate_kpath_1d(structure, kpoints_distance) + elif structure.pbc == (True, True, False): + kpoints = generate_kpath_2d( + structure=structure, + kpoints_distance=kpoints_distance, + kpath_2d=determine_symmetry_path(structure), + ) + + if simulation_mode == "normal": + builder.bands.pop("bands_kpoints_distance", None) + builder.bands.update({"bands_kpoints": kpoints}) + elif simulation_mode == "fat_bands": + builder.bands_projwfc.pop("bands_kpoints_distance", None) + builder.bands_projwfc.update({"bands_kpoints": kpoints}) + + builder.structure = structure + return builder + + def setup(self): + """Define the current workchain""" + self.ctx.current_structure = self.inputs.structure + if "bands" in self.inputs: + self.ctx.key = "bands" + self.ctx.workchain = PwBandsWorkChain + elif "bands_projwfc" in self.inputs: + self.ctx.key = "bands_projwfc" + self.ctx.workchain = ProjwfcBandsWorkChain + else: + self.report("No bands workchain specified") + return self.exit_codes.ERROR_WORKCHAIN_FAILED + + def run_bands(self): + """Run the bands workchain""" + inputs = AttributeDict( + self.exposed_inputs(self.ctx.workchain, namespace=self.ctx.key) + ) + inputs.metadata.call_link_label = self.ctx.key + inputs.structure = self.ctx.current_structure + future = self.submit(self.ctx.workchain, **inputs) + self.report(f"submitting `WorkChain` <PK={future.pk}>") + self.to_context(**{self.ctx.key: future}) + + def results(self): + """Attach the bands results""" + workchain = self.ctx[self.ctx.key] + + if not workchain.is_finished_ok: + self.report("Bands workchain failed") + return self.exit_codes.ERROR_WORKCHAIN_FAILED + else: + self.out_many( + self.exposed_outputs( + self.ctx[self.ctx.key], self.ctx.workchain, namespace=self.ctx.key + ) + ) + self.report("Bands workchain completed successfully") diff --git a/src/aiidalab_qe/plugins/bands/result.py b/src/aiidalab_qe/plugins/bands/result.py index 03b7630cd..7ca4b206c 100644 --- a/src/aiidalab_qe/plugins/bands/result.py +++ b/src/aiidalab_qe/plugins/bands/result.py @@ -14,11 +14,22 @@ def __init__(self, node=None, **kwargs): super().__init__(node=node, **kwargs) def _update_view(self): - # Check if the workchain has the outputs - try: - bands_node = self.node.outputs.bands - except AttributeError: - bands_node = None + # Initialize bands_node to None by default + bands_node = None + + # Check if the workchain has the 'bands' output + if hasattr(self.node.outputs, "bands"): + bands_output = self.node.outputs.bands + + # Check for 'bands' or 'bands_projwfc' attributes within 'bands' output + if hasattr(bands_output, "bands"): + bands_node = bands_output.bands + elif hasattr(bands_output, "bands_projwfc"): + bands_node = bands_output.bands_projwfc + else: + # If neither 'bands' nor 'bands_projwfc' exist, use 'bands_output' itself + # This is the case for compatibility with older versions of the plugin + bands_node = bands_output _bands_plot_view = BandPdosWidget(bands=bands_node) self.children = [ diff --git a/src/aiidalab_qe/plugins/bands/setting.py b/src/aiidalab_qe/plugins/bands/setting.py index 12f012279..05ff99e4e 100644 --- a/src/aiidalab_qe/plugins/bands/setting.py +++ b/src/aiidalab_qe/plugins/bands/setting.py @@ -18,39 +18,37 @@ def __init__(self, **kwargs): options=["fast", "moderate", "precise"], value="moderate", ) - self.kpath_2d_help = ipw.HTML( - """<div style="line-height: 140%; padding-top: 0px; padding-bottom: 5px"> - If your system has periodicity xy. Please select one of the five 2D Bravais lattices corresponding to your system. - </div>""" + self.properties_help = ipw.HTML( + """<div style="line-height: 140%; padding-top: 10px; padding-bottom: 0px"> + The band structure workflow will + automatically detect the default path in reciprocal space using the + <a href="https://www.materialscloud.org/work/tools/seekpath" target="_blank"> + SeeK-path tool</a>. + <br><br> + Fat Bands is a band structure plot that includes the angular momentum contributions from specific atoms or orbitals to each energy band. The thickness of the bands represents the strength of these contributions, providing insight into the electronic structure. + </div>""" ) - self.kpath_2d = ipw.Dropdown( - description="Lattice:", - options=[ - ("Hexagonal", "hexagonal"), - ("Square", "square"), - ("Rectangular", "rectangular"), - ("Centered Rectangular", "centered_rectangular"), - ("Oblique", "oblique"), - ], - value="hexagonal", + self.projwfc_bands = ipw.Checkbox( + description="Fat bands calculation", + value=False, + style={"description_width": "initial"}, ) self.children = [ self.settings_title, - self.kpath_2d_help, - self.kpath_2d, + self.properties_help, + self.projwfc_bands, ] super().__init__(**kwargs) def get_panel_value(self): """Return a dictionary with the input parameters for the plugin.""" return { - "kpath_2d": self.kpath_2d.value, + "projwfc_bands": self.projwfc_bands.value, } def set_panel_value(self, input_dict): - """Load a dictionary with the input parameters for the plugin.""" - self.kpath_2d.value = input_dict.get("kpath_2d", "hexagonal") + self.projwfc_bands.value = input_dict.get("projwfc_bands", False) def reset(self): """Reset the panel to its default values.""" - self.kpath_2d.value = "hexagonal" + self.projwfc_bands.value = False diff --git a/src/aiidalab_qe/plugins/bands/workchain.py b/src/aiidalab_qe/plugins/bands/workchain.py index c3cc5eb78..863a7dc0b 100644 --- a/src/aiidalab_qe/plugins/bands/workchain.py +++ b/src/aiidalab_qe/plugins/bands/workchain.py @@ -1,180 +1,48 @@ -import numpy as np - -from aiida.plugins import DataFactory, WorkflowFactory +from aiida.plugins import WorkflowFactory from aiida_quantumespresso.common.types import ElectronicType, SpinType from aiidalab_qe.plugins.utils import set_component_resources -GAMMA = "\u0393" - -PwBandsWorkChain = WorkflowFactory("quantumespresso.pw.bands") -KpointsData = DataFactory("core.array.kpoints") - +BandsWorkChain = WorkflowFactory("aiidalab_qe.bands_workchain") +# from .bands_workchain import BandsWorkChain -def points_per_branch(vector_a, vector_b, reciprocal_cell, bands_kpoints_distance): - """function to calculate the number of points per branch depending on the kpoints_distance and the reciprocal cell""" - scaled_vector_a = np.array(vector_a) - scaled_vector_b = np.array(vector_b) - reciprocal_vector_a = scaled_vector_a.dot(reciprocal_cell) - reciprocal_vector_b = scaled_vector_b.dot(reciprocal_cell) - distance = np.linalg.norm(reciprocal_vector_a - reciprocal_vector_b) - return max( - 2, int(np.round(distance / bands_kpoints_distance)) - ) # at least two points for each segment, including both endpoints explicitly - -def calculate_bands_kpoints_distance(kpoints_distance): - """function to calculate the bands_kpoints_distance depending on the kpoints_distance""" - if kpoints_distance >= 0.5: - return 0.1 - elif 0.15 < kpoints_distance < 0.5: - return 0.025 - else: - return 0.015 - - -def generate_kpath_1d(structure, kpoints_distance): - """Return a kpoints object for one dimensional systems (from Gamma to X) - The number of kpoints is calculated based on the kpoints_distance (as in the PwBandsWorkChain protocol) - """ - kpoints = KpointsData() - kpoints.set_cell_from_structure(structure) - reciprocal_cell = kpoints.reciprocal_cell - bands_kpoints_distance = calculate_bands_kpoints_distance(kpoints_distance) - - # Number of points per branch - num_points_per_branch = points_per_branch( - [0.0, 0.0, 0.0], - [0.5, 0.0, 0.0], - reciprocal_cell, - bands_kpoints_distance, - ) - # Generate the kpoints - points = np.linspace( - start=[0.0, 0.0, 0.0], - stop=[0.5, 0.0, 0.0], - endpoint=True, - num=num_points_per_branch, - ) - kpoints.set_kpoints(points.tolist()) - kpoints.labels = [[0, GAMMA], [len(points) - 1, "X"]] - return kpoints - - -def generate_kpath_2d(structure, kpoints_distance, kpath_2d): - """Return a kpoints object for two dimensional systems based on the selected 2D symmetry path - The number of kpoints is calculated based on the kpoints_distance (as in the PwBandsWorkChain protocol) - The 2D symmetry paths are defined as in The Journal of Physical Chemistry Letters 2022 13 (50), 11581-11594 (https://pubs.acs.org/doi/10.1021/acs.jpclett.2c02972) - """ - kpoints = KpointsData() - kpoints.set_cell_from_structure(structure) - reciprocal_cell = kpoints.reciprocal_cell - bands_kpoints_distance = calculate_bands_kpoints_distance(kpoints_distance) - - # dictionary with the 2D symmetry paths - selected_paths = { - "hexagonal": { - "path": [ - [0.0, 0.0, 0.0], - [0.5, 0.0, 0.0], - [0.33333, 0.33333, 0.0], - [1.0, 0.0, 0.0], - ], - "labels": [GAMMA, "M", "K", GAMMA], - }, - "square": { - "path": [ - [0.0, 0.0, 0.0], - [0.5, 0.0, 0.0], - [0.5, 0.5, 0.0], - [1.0, 0.0, 0.0], - ], - "labels": [GAMMA, "X", "M", GAMMA], - }, - "rectangular": { - "path": [ - [0.0, 0.0, 0.0], - [0.5, 0.0, 0.0], - [0.5, 0.5, 0.0], - [0.0, 0.5, 0.0], - [1.0, 0.0, 0.0], - ], - "labels": [GAMMA, "X", "S", "Y", GAMMA], - }, - } - # if the selected path is centered_rectangular or oblique, the path is calculated based on the reciprocal cell - if kpath_2d in ["centered_rectangular", "oblique"]: - a1 = reciprocal_cell[0] - a2 = reciprocal_cell[1] - norm_a1 = np.linalg.norm(a1) - norm_a2 = np.linalg.norm(a2) - cos_gamma = ( - a1.dot(a2) / (norm_a1 * norm_a2) - ) # Angle between a1 and a2 # like in https://pubs.acs.org/doi/10.1021/acs.jpclett.2c02972 - gamma = np.arccos(cos_gamma) - eta = (1 - (norm_a1 / norm_a2) * cos_gamma) / (2 * np.power(np.sin(gamma), 2)) - nu = 0.5 - (eta * norm_a2 * cos_gamma) / norm_a1 - selected_paths["centered_rectangular"] = { - "path": [ - [0.0, 0.0, 0.0], - [0.5, 0.0, 0.0], - [1 - eta, nu, 0], - [0.5, 0.5, 0.0], - [eta, 1 - nu, 0.0], - [1.0, 0.0, 0.0], - ], - "labels": [GAMMA, "X", "H_1", "C", "H", GAMMA], - } - selected_paths["oblique"] = { - "path": [ - [0.0, 0.0, 0.0], - [0.5, 0.0, 0.0], - [1 - eta, nu, 0], - [0.5, 0.5, 0.0], - [eta, 1 - nu, 0.0], - [0.0, 0.5, 0.0], - [1.0, 0.0, 0.0], - ], - "labels": [GAMMA, "X", "H_1", "C", "H", "Y", GAMMA], - } - path = selected_paths[kpath_2d]["path"] - labels = selected_paths[kpath_2d]["labels"] - branches = zip(path[:-1], path[1:]) - - all_kpoints = [] # List to hold all k-points - label_map = [] # List to hold labels and their corresponding k-point indices - - # Calculate the number of points per branch and generate the kpoints - index_offset = 0 # Start index for each segment - for (start, end), label_start, _label_end in zip(branches, labels[:-1], labels[1:]): - num_points_per_branch = points_per_branch( - start, end, reciprocal_cell, bands_kpoints_distance +def check_codes(pw_code, projwfc_code): + """Check that the codes are installed on the same computer.""" + if ( + not any( + [ + pw_code is None, + projwfc_code is None, + ] + ) + and len( + { + pw_code.computer.pk, + projwfc_code.computer.pk, + } + ) + != 1 + ): + raise ValueError( + "All selected codes must be installed on the same computer. This is because the " + "BandsWorkChain calculations rely on large files that are not retrieved by AiiDA." ) - # Exclude endpoint except for the last segment to prevent duplication - points = np.linspace(start, end, num=num_points_per_branch, endpoint=False) - all_kpoints.extend(points) - label_map.append( - (index_offset, label_start) - ) # Label for the start of the segment - index_offset += len(points) - - # Include the last point and its label - all_kpoints.append(path[-1]) - label_map.append((index_offset, labels[-1])) # Label for the last point - - # Set the kpoints and their labels in KpointsData - kpoints.set_kpoints(all_kpoints) - kpoints.labels = label_map - - return kpoints def update_resources(builder, codes): - set_component_resources(builder.scf.pw, codes.get("pw")) - set_component_resources(builder.bands.pw, codes.get("pw")) + if "bands" in builder: + set_component_resources(builder.bands.scf.pw, codes.get("pw")) + set_component_resources(builder.bands.bands.pw, codes.get("pw")) + elif "bands_projwfc" in builder: + set_component_resources(builder.bands_projwfc.scf.pw, codes.get("pw")) + set_component_resources(builder.bands_projwfc.bands.pw, codes.get("pw")) + set_component_resources( + builder.bands_projwfc.projwfc.projwfc, codes.get("projwfc_bands") + ) def get_builder(codes, structure, parameters, **kwargs): - """Get a builder for the PwBandsWorkChain.""" + """Get a builder for the BandsWorkChain.""" from copy import deepcopy pw_code = codes.get("pw")["code"] @@ -188,14 +56,25 @@ def get_builder(codes, structure, parameters, **kwargs): bands_overrides.pop("kpoints_distance", None) bands_overrides["pw"]["parameters"]["SYSTEM"].pop("smearing", None) bands_overrides["pw"]["parameters"]["SYSTEM"].pop("degauss", None) + + check_codes(pw_code, codes.get("projwfc_bands")["code"]) + overrides = { "scf": scf_overrides, "bands": bands_overrides, "relax": relax_overrides, } - bands = PwBandsWorkChain.get_builder_from_protocol( - code=pw_code, + + if parameters["bands"]["projwfc_bands"]: + simulation_mode = "fat_bands" + else: + simulation_mode = "normal" + + bands_builder = BandsWorkChain.get_builder_from_protocol( + pw_code=pw_code, + projwfc_code=codes.get("projwfc_bands")["code"], structure=structure, + simulation_mode=simulation_mode, protocol=protocol, electronic_type=ElectronicType(parameters["workchain"]["electronic_type"]), spin_type=SpinType(parameters["workchain"]["spin_type"]), @@ -203,44 +82,18 @@ def get_builder(codes, structure, parameters, **kwargs): overrides=overrides, **kwargs, ) + update_resources(bands_builder, codes) - if structure.pbc != (True, True, True): - kpoints_distance = parameters["advanced"]["kpoints_distance"] - if structure.pbc == (True, False, False): - kpoints = generate_kpath_1d(structure, kpoints_distance) - elif structure.pbc == (True, True, False): - kpoints = generate_kpath_2d( - structure, kpoints_distance, parameters["bands"]["kpath_2d"] - ) - bands.pop("bands_kpoints_distance") - bands.update({"bands_kpoints": kpoints}) - - # pop the inputs that are excluded from the expose_inputs - bands.pop("relax") - bands.pop("structure", None) - bands.pop("clean_workdir", None) - # update resources - update_resources(bands, codes) - - 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 + return bands_builder def update_inputs(inputs, ctx): """Update the inputs using context.""" inputs.structure = ctx.current_structure - inputs.scf.pw.parameters = inputs.scf.pw.parameters.get_dict() - if ctx.current_number_of_bands: - inputs.scf.pw.parameters.setdefault("SYSTEM", {}).setdefault( - "nbnd", ctx.current_number_of_bands - ) workchain_and_builder = { - "workchain": PwBandsWorkChain, + "workchain": BandsWorkChain, "exclude": ("structure", "relax"), "get_builder": get_builder, "update_inputs": update_inputs, diff --git a/src/aiidalab_qe/plugins/electronic_structure/result.py b/src/aiidalab_qe/plugins/electronic_structure/result.py index 1142ae450..890e92c3a 100644 --- a/src/aiidalab_qe/plugins/electronic_structure/result.py +++ b/src/aiidalab_qe/plugins/electronic_structure/result.py @@ -19,10 +19,23 @@ def _update_view(self): except AttributeError: pdos_node = None - try: - bands_node = self.node.outputs.bands - except AttributeError: - bands_node = None + # Initialize bands_node to None by default + bands_node = None + + # Check if the workchain has the 'bands' output + if hasattr(self.node.outputs, "bands"): + bands_output = self.node.outputs.bands + + # Check for 'bands' or 'bands_projwfc' attributes within 'bands' output + if hasattr(bands_output, "bands"): + bands_node = bands_output.bands + elif hasattr(bands_output, "bands_projwfc"): + bands_node = bands_output.bands_projwfc + else: + # If neither 'bands' nor 'bands_projwfc' exist, use 'bands_output' itself + # This is the case for compatibility with older versions of the plugin + bands_node = bands_output + _bands_dos_widget = BandPdosWidget(bands=bands_node, pdos=pdos_node) # update the electronic structure tab self.children = [_bands_dos_widget] diff --git a/tests/conftest.py b/tests/conftest.py index c65842d28..20d9bae4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,6 +90,14 @@ def _generate_structure_data(name="silicon", pbc=(True, True, True)): for site in sites: structure.append_atom(position=site[2], symbols=site[0], name=site[1]) + + elif name == "MoS2": + cell = [[3.1922, 0, 0], [-1.5961, 2.7646, 0], [0, 0, 13.3783]] + structure = orm.StructureData(cell=cell) + structure.append_atom(position=(-0.0, 1.84, 10.03), symbols="Mo") + structure.append_atom(position=(1.6, 0.92, 8.47), symbols="S") + structure.append_atom(position=(1.6, 0.92, 11.6), symbols="S") + structure.pbc = pbc return structure @@ -289,6 +297,16 @@ def projwfc_code(aiida_local_code_factory): ) +@pytest.fixture +def projwfc_bands_code(aiida_local_code_factory): + """Return a `Code` configured for the projwfc.x executable.""" + return aiida_local_code_factory( + label="projwfc_bands", + executable="bash", + entry_point="quantumespresso.projwfc", + ) + + @pytest.fixture() def workchain_settings_generator(): """Return a function that generates a workchain settings dictionary.""" @@ -316,7 +334,7 @@ def _smearing_settings_generator(**kwargs): @pytest.fixture -def app(pw_code, dos_code, projwfc_code): +def app(pw_code, dos_code, projwfc_code, projwfc_bands_code): from aiidalab_qe.app.main import App # Since we use `qe_auto_setup=False`, which will skip the pseudo library installation @@ -329,10 +347,12 @@ def app(pw_code, dos_code, projwfc_code): app.submit_step.pw_code.code_selection.refresh() app.submit_step.codes["dos"].code_selection.refresh() app.submit_step.codes["projwfc"].code_selection.refresh() + app.submit_step.codes["projwfc_bands"].code_selection.refresh() app.submit_step.pw_code.value = pw_code.uuid app.submit_step.codes["dos"].value = dos_code.uuid app.submit_step.codes["projwfc"].value = projwfc_code.uuid + app.submit_step.codes["projwfc_bands"].value = projwfc_bands_code.uuid yield app @@ -558,17 +578,17 @@ def generate_bands_workchain( """Generate an instance of a the WorkChain.""" def _generate_bands_workchain(structure): - from copy import deepcopy - from aiida import engine from aiida.orm import Dict - from aiida_quantumespresso.workflows.pw.bands import PwBandsWorkChain + from aiidalab_qe.plugins.bands.bands_workchain import BandsWorkChain pseudo_family = f"SSSP/{SSSP_VERSION}/PBEsol/efficiency" inputs = { - "code": fixture_code("quantumespresso.pw"), + "pw_code": fixture_code("quantumespresso.pw"), + "projwfc_code": fixture_code("quantumespresso.projwfc"), "structure": structure, + "simulation_mode": "normal", "overrides": { "scf": { "pseudo_family": pseudo_family, @@ -586,20 +606,31 @@ def _generate_bands_workchain(structure): }, }, } - builder = PwBandsWorkChain.get_builder_from_protocol(**inputs) + builder = BandsWorkChain.get_builder_from_protocol(**inputs) inputs = builder._inputs() - inputs["relax"]["base_final_scf"] = deepcopy(inputs["relax"]["base"]) - wkchain = generate_workchain(PwBandsWorkChain, inputs) + wkchain = generate_workchain(BandsWorkChain, inputs) wkchain.setup() # run bands and return the process - output_parameters = Dict(dict={"fermi_energy": 2.0}) - output_parameters.store() - wkchain.out("scf_parameters", output_parameters) - wkchain.out("band_parameters", output_parameters) + fermi_dict = Dict(dict={"fermi_energy": 2.0}) + fermi_dict.store() + output_parameters = { + "bands": { + "scf_parameters": fermi_dict, + "band_parameters": fermi_dict, + } + } + + wkchain.out( + "bands.scf_parameters", output_parameters["bands"]["scf_parameters"] + ) + wkchain.out( + "bands.band_parameters", output_parameters["bands"]["band_parameters"] + ) + # band_structure = generate_bands_data() band_structure.store() - wkchain.out("band_structure", band_structure) + wkchain.out("bands.band_structure", band_structure) wkchain.update_outputs() # bands_node = wkchain.node @@ -616,6 +647,7 @@ def generate_qeapp_workchain( generate_workchain, generate_pdos_workchain, generate_bands_workchain, + fixture_code, ): """Generate an instance of the WorkChain.""" @@ -632,6 +664,7 @@ def _generate_qeapp_workchain( ): from copy import deepcopy + from aiida.orm import Dict from aiida.orm.utils.serialize import serialize from aiidalab_qe.app.configuration import ConfigureQeAppWorkChainStep from aiidalab_qe.app.submission import SubmitQeAppWorkChainStep @@ -677,13 +710,37 @@ def _generate_qeapp_workchain( # step 3 setup code and resources s3: SubmitQeAppWorkChainStep = app.submit_step s3.pw_code.num_cpus.value = 4 + builder = s3._create_builder() inputs = builder._inputs() inputs["relax"]["base_final_scf"] = deepcopy(inputs["relax"]["base"]) + + # Setting up inputs for bands_projwfc + inputs["bands"]["bands_projwfc"]["scf"]["pw"] = deepcopy( + inputs["bands"]["bands"]["scf"]["pw"] + ) + inputs["bands"]["bands_projwfc"]["bands"]["pw"] = deepcopy( + inputs["bands"]["bands"]["bands"]["pw"] + ) + inputs["bands"]["bands_projwfc"]["bands"]["pw"]["code"] = inputs["bands"][ + "bands" + ]["bands"]["pw"]["code"] + inputs["bands"]["bands_projwfc"]["scf"]["pw"]["code"] = inputs["bands"][ + "bands" + ]["scf"]["pw"]["code"] + + inputs["bands"]["bands_projwfc"]["projwfc"]["projwfc"]["code"] = fixture_code( + "quantumespresso.projwfc" + ) + inputs["bands"]["bands_projwfc"]["projwfc"]["projwfc"]["parameters"] = Dict( + {"PROJWFC": {"DeltaE": 0.01}} + ).store() + if run_bands: inputs["properties"].append("bands") if run_pdos: inputs["properties"].append("pdos") + wkchain = generate_workchain(QeAppWorkChain, inputs) wkchain.setup() # mock output @@ -697,11 +754,11 @@ def _generate_qeapp_workchain( wkchain.exposed_outputs(pdos.node, PdosWorkChain, namespace="pdos") ) if run_bands: - from aiida_quantumespresso.workflows.pw.bands import PwBandsWorkChain + from aiidalab_qe.plugins.bands.bands_workchain import BandsWorkChain bands = generate_bands_workchain(structure) wkchain.out_many( - wkchain.exposed_outputs(bands.node, PwBandsWorkChain, namespace="bands") + wkchain.exposed_outputs(bands.node, BandsWorkChain, namespace="bands") ) wkchain.update_outputs() # set ui_parameters diff --git a/tests/test_plugins_bands.py b/tests/test_plugins_bands.py index aaa34225a..00e7a75c8 100644 --- a/tests/test_plugins_bands.py +++ b/tests/test_plugins_bands.py @@ -39,15 +39,22 @@ def test_result(generate_qeapp_workchain): def test_structure_1d(generate_qeapp_workchain, generate_structure_data): structure = generate_structure_data("silicon", pbc=(True, False, False)) wkchain = generate_qeapp_workchain(structure=structure) - assert "bands_kpoints_distance" not in wkchain.inputs.bands - assert "bands_kpoints" in wkchain.inputs.bands - assert len(wkchain.inputs.bands.bands_kpoints.labels) == 2 + assert "bands_kpoints_distance" not in wkchain.inputs.bands.bands + assert "bands_kpoints" in wkchain.inputs.bands.bands + assert len(wkchain.inputs.bands.bands.bands_kpoints.labels) == 2 + assert wkchain.inputs.bands.bands.bands_kpoints.labels == [(0, "Γ"), (9, "X")] @pytest.mark.usefixtures("sssp") def test_structure_2d(generate_qeapp_workchain, generate_structure_data): - structure = generate_structure_data("silicon", pbc=(True, True, False)) + structure = generate_structure_data("MoS2", pbc=(True, True, False)) wkchain = generate_qeapp_workchain(structure=structure) - assert "bands_kpoints_distance" not in wkchain.inputs.bands - assert "bands_kpoints" in wkchain.inputs.bands - assert len(wkchain.inputs.bands.bands_kpoints.labels) == 4 + assert "bands_kpoints_distance" not in wkchain.inputs.bands.bands + assert "bands_kpoints" in wkchain.inputs.bands.bands + assert len(wkchain.inputs.bands.bands.bands_kpoints.labels) == 4 + assert wkchain.inputs.bands.bands.bands_kpoints.labels == [ + (0, "Γ"), + (11, "M"), + (18, "K"), + (31, "Γ"), + ] diff --git a/tests/test_submit_qe_workchain.py b/tests/test_submit_qe_workchain.py index b21f1e38f..181554392 100644 --- a/tests/test_submit_qe_workchain.py +++ b/tests/test_submit_qe_workchain.py @@ -73,8 +73,11 @@ def test_create_builder_insulator( # check and validate the builder got = builder_to_readable_dict(builder) - assert got["bands"]["scf"]["pw"]["parameters"]["SYSTEM"]["occupations"] == "fixed" - assert "smearing" not in got["bands"]["scf"]["pw"]["parameters"]["SYSTEM"] + assert ( + got["bands"]["bands"]["scf"]["pw"]["parameters"]["SYSTEM"]["occupations"] + == "fixed" + ) + assert "smearing" not in got["bands"]["bands"]["scf"]["pw"]["parameters"]["SYSTEM"] @pytest.mark.usefixtures("sssp") @@ -111,7 +114,7 @@ def test_create_builder_advanced_settings( # test tot_charge is updated in the three steps for parameters in [ got["relax"]["base"], - got["bands"]["scf"], + got["bands"]["bands"]["scf"], got["pdos"]["scf"], got["pdos"]["nscf"], ]: diff --git a/tests/test_submit_qe_workchain/test_create_builder_default.yml b/tests/test_submit_qe_workchain/test_create_builder_default.yml index 4b9f604cc..3ad9d35b7 100644 --- a/tests/test_submit_qe_workchain/test_create_builder_default.yml +++ b/tests/test_submit_qe_workchain/test_create_builder_default.yml @@ -20,7 +20,7 @@ advanced: vdw_corr: none pseudos: {} bands: - kpath_2d: hexagonal + projwfc_bands: false codes: dos: cpus: 1 @@ -34,6 +34,12 @@ codes: max_wallclock_seconds: 43200 nodes: 1 ntasks_per_node: 1 + projwfc_bands: + cpus: 1 + cpus_per_task: 1 + max_wallclock_seconds: 43200 + nodes: 1 + ntasks_per_node: 1 pw: cpus: 2 cpus_per_task: 1