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 0bf2938e8..fd7e288ec 100644
--- a/src/aiidalab_qe/app/configuration/workflow.py
+++ b/src/aiidalab_qe/app/configuration/workflow.py
@@ -154,12 +154,12 @@ def render(self):
self.spin_type,
]
),
+ *self.property_children,
ipw.HTML("""
Protocol
"""),
- *self.property_children,
ipw.HTML("Select the protocol:", layout=ipw.Layout(flex="1 1 auto")),
self.protocol,
ipw.HTML("""
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 5a16f9316..54fa6842a 100644
--- a/src/aiidalab_qe/plugins/bands/__init__.py
+++ b/src/aiidalab_qe/plugins/bands/__init__.py
@@ -1,4 +1,5 @@
# from aiidalab_qe.bands.result import Result
+from aiidalab_qe.app.submission.code.model import CodeModel
from aiidalab_qe.common.panel import PanelOutline
from .model import BandsModel
@@ -19,6 +20,12 @@ class BandsOutline(PanelOutline):
bands = {
"outline": BandsOutline,
"model": BandsModel,
+ "code": {
+ "projwfc_bands": CodeModel(
+ description="projwfc.x",
+ default_calc_job_plugin="quantumespresso.projwfc",
+ ),
+ },
"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` ")
+ 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/model.py b/src/aiidalab_qe/plugins/bands/model.py
index c9c8ad851..1f5b57b26 100644
--- a/src/aiidalab_qe/plugins/bands/model.py
+++ b/src/aiidalab_qe/plugins/bands/model.py
@@ -6,18 +6,13 @@
class BandsModel(SettingsModel):
"""Model for the band structure plugin."""
- kpath_2d = tl.Unicode("hexagonal")
+ projwfc_bands = tl.Bool(False)
def get_model_state(self):
- return {
- "kpath_2d": self.kpath_2d,
- }
+ return {"projwfc_bands": self.projwfc_bands}
def set_model_state(self, parameters: dict):
- self.kpath_2d = parameters.get(
- "kpath_2d",
- self.traits()["kpath_2d"].default_value,
- )
+ self.projwfc_bands = parameters.get("projwfc_bands", False)
def reset(self):
- self.kpath_2d = self.traits()["kpath_2d"].default_value
+ self.kpath_2d = False
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 341011356..abf4cdc86 100644
--- a/src/aiidalab_qe/plugins/bands/setting.py
+++ b/src/aiidalab_qe/plugins/bands/setting.py
@@ -13,49 +13,35 @@ def render(self):
if self.rendered:
return
- self.settings_title = ipw.HTML(
- """
-
Settings
"""
- )
- self.protocol = ipw.ToggleButtons(
- options=["fast", "moderate", "precise"],
+ self.projwfc_bands = ipw.Checkbox(
+ description="Fat bands calculation",
+ style={"description_width": "initial"},
)
ipw.link(
- (self._config_model.workchain, "protocol"),
- (self.protocol, "value"),
- )
-
- self.kpath_2d_help = ipw.HTML(
- """
- If your system has periodicity xy. Please select one of the five 2D Bravais lattices corresponding to your system.
-
"""
- )
- self.kpath_2d = ipw.Dropdown(
- description="Lattice:",
- options=[
- ("Hexagonal", "hexagonal"),
- ("Square", "square"),
- ("Rectangular", "rectangular"),
- ("Centered Rectangular", "centered_rectangular"),
- ("Oblique", "oblique"),
- ],
- )
- ipw.link(
- (self._model, "kpath_2d"),
- (self.kpath_2d, "value"),
+ (self._model, "projwfc_bands"),
+ (self.projwfc_bands, "value"),
)
self.children = [
- self.settings_title,
- self.kpath_2d_help,
- self.kpath_2d,
+ ipw.HTML("""
+
+
Settings
+
+ """),
ipw.HTML("""
The band structure workflow will automatically detect the default
path in reciprocal space using the
SeeK-path tool.
+
+ 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.
"""),
+ self.projwfc_bands,
]
self.rendered = True
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 64a0ff8fb..af07e8619 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,6 +13,16 @@
pytest_plugins = ["aiida.manage.tests.pytest_fixtures"]
+ELEMENTS = [
+ "H",
+ "Li",
+ "O",
+ "S",
+ "Si",
+ "Co",
+ "Mo",
+]
+
@pytest.fixture
def fixture_localhost(aiida_localhost):
@@ -92,6 +102,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
@@ -195,7 +213,7 @@ def sssp(generate_upf_data):
with tempfile.TemporaryDirectory() as d:
dirpath = pathlib.Path(d)
- for element in ("H", "Li", "O", "Si", "Co"):
+ for element in ELEMENTS:
upf = generate_upf_data(element)
filename = dirpath / f"{element}.upf"
@@ -227,7 +245,7 @@ def pseudodojo(generate_upf_data):
with tempfile.TemporaryDirectory() as d:
dirpath = pathlib.Path(d)
- for element in ("H", "Li", "O", "Si", "Co"):
+ for element in ELEMENTS:
upf = generate_upf_data(element)
filename = dirpath / f"{element}.upf"
@@ -303,6 +321,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."""
@@ -332,7 +360,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
app = App(qe_auto_setup=False)
@@ -348,14 +376,17 @@ def app(pw_code, dos_code, projwfc_code):
# set up codes
app.submit_model.get_code("pdos", "dos").activate()
app.submit_model.get_code("pdos", "projwfc").activate()
+ app.submit_model.get_code("bands", "projwfc_bands").activate()
app.submit_model.code_widgets["pw"].code_selection.refresh()
app.submit_model.code_widgets["dos"].code_selection.refresh()
app.submit_model.code_widgets["projwfc"].code_selection.refresh()
+ app.submit_model.code_widgets["projwfc_bands"].code_selection.refresh()
app.submit_model.code_widgets["pw"].value = pw_code.uuid
app.submit_model.code_widgets["dos"].value = dos_code.uuid
app.submit_model.code_widgets["projwfc"].value = projwfc_code.uuid
+ app.submit_model.code_widgets["projwfc_bands"].value = projwfc_bands_code.uuid
# TODO overrides app defaults - check!
app.submit_model.set_selected_codes(
@@ -363,6 +394,7 @@ def app(pw_code, dos_code, projwfc_code):
"pw": {"code": pw_code.label},
"dos": {"code": dos_code.label},
"projwfc": {"code": projwfc_code.label},
+ "projwfc_bands": {"code": projwfc_bands_code.label},
}
)
@@ -591,17 +623,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,
@@ -619,20 +651,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
@@ -649,6 +692,7 @@ def generate_qeapp_workchain(
generate_workchain,
generate_pdos_workchain,
generate_bands_workchain,
+ fixture_code,
):
"""Generate an instance of the WorkChain."""
@@ -665,6 +709,7 @@ def _generate_qeapp_workchain(
):
from copy import deepcopy
+ from aiida.orm import Dict
from aiida.orm.utils.serialize import serialize
from aiidalab_qe.workflows import QeAppWorkChain
@@ -718,12 +763,36 @@ def _generate_qeapp_workchain(
app.submit_model.code_widgets["pw"].num_cpus.value = 4
parameters = app.submit_model._get_submission_parameters()
builder = app.submit_model._create_builder(parameters)
+
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")
+
workchain = generate_workchain(QeAppWorkChain, inputs)
workchain.setup()
@@ -742,13 +811,13 @@ def _generate_qeapp_workchain(
)
)
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)
workchain.out_many(
workchain.exposed_outputs(
bands.node,
- PwBandsWorkChain,
+ BandsWorkChain,
namespace="bands",
)
)
diff --git a/tests/test_plugins_bands.py b/tests/test_plugins_bands.py
index 55692756b..05b4b4e19 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("aiida_profile_clean", "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 8763c6c76..1a5955691 100644
--- a/tests/test_submit_qe_workchain.py
+++ b/tests/test_submit_qe_workchain.py
@@ -71,8 +71,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("aiida_profile_clean", "sssp")
@@ -108,7 +111,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 63597b234..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
@@ -48,9 +54,9 @@ pdos:
workchain:
electronic_type: metal
properties:
- - bands
- - pdos
- - relax
+ - bands
+ - pdos
+ - relax
protocol: moderate
relax_type: positions_cell
spin_type: none